From ee55406aeea80905b6004c63a4250f5b8bf22da8 Mon Sep 17 00:00:00 2001 From: Ron Gummich Date: Fri, 24 Jul 2026 14:50:14 +0200 Subject: [PATCH] feat(gateway): refactor gateway to prepare to envoy-gateway --- .opencode/README.md | 54 ++ .opencode/agent/envoy-expert/enovy-expert.md | 35 + .../agent/kgateway-expert/kgateway-expert.md | 34 + .../agent/strict-reviewer/requirements.md | 237 +++++ .../agent/strict-reviewer/strict-reviewer.md | 35 + .opencode/skills/evaluate-envoy/SKILL.md | 49 + .opencode/skills/gateway-expert/SKILL.md | 155 +++ .opencode/skills/implement-envoy/SKILL.md | 195 ++++ .opencode/skills/map-logic/SKILL.md | 94 ++ .../skills/map-logic/lms-token-issuing.md | 88 ++ gateway/api/v1/gateway_types.go | 10 + gateway/cmd/main.go | 19 +- .../gateway.cp.ei.telekom.de_gateways.yaml | 5 + gateway/go.mod | 10 +- gateway/go.sum | 12 + .../internal/controller/route_controller.go | 7 +- gateway/internal/features/envoy/README.md | 207 ++++ .../features/envoy/access_control_test.go | 181 ++++ gateway/internal/features/envoy/builder.go | 149 +++ .../internal/features/envoy/builder_test.go | 175 ++++ .../features/envoy/feature/access_control.go | 232 +++++ gateway/internal/features/envoy/nodehash.go | 60 ++ gateway/internal/features/envoy/routing.go | 180 ++++ gateway/internal/features/envoy/server.go | 101 ++ gateway/internal/features/envoy/suite_test.go | 17 + gateway/internal/features/envoy/xds.go | 163 ++++ gateway/internal/features/envoy/xds_test.go | 75 ++ gateway/internal/features/errors.go | 12 + gateway/internal/features/interfaces.go | 82 ++ .../internal/features/{ => kong}/builder.go | 91 +- .../features/{ => kong}/builder_test.go | 177 ++-- .../{ => kong}/feature/access_control.go | 6 +- .../{ => kong}/feature/access_control_test.go | 6 +- .../features/{ => kong}/feature/basic_auth.go | 6 +- .../{ => kong}/feature/basic_auth_test.go | 6 +- .../{ => kong}/feature/circuit_breaker.go | 12 +- .../feature/circuit_breaker_test.go | 6 +- .../features/{ => kong}/feature/claims.go | 6 +- .../{ => kong}/feature/claims_test.go | 6 +- .../feature/config/circuit_breaker_config.go | 0 .../{ => kong}/feature/custom_scopes.go | 6 +- .../{ => kong}/feature/custom_scopes_test.go | 6 +- .../{ => kong}/feature/dynamic_upstream.go | 14 +- .../feature/dynamic_upstream_test.go | 6 +- .../{ => kong}/feature/external_idp.go | 6 +- .../{ => kong}/feature/external_idp_test.go | 6 +- .../features/{ => kong}/feature/failover.go | 6 +- .../{ => kong}/feature/failover_test.go | 6 +- .../feature/header_transformation.go | 6 +- .../feature/header_transformation_test.go | 6 +- .../{ => kong}/feature/iprestriction.go | 6 +- .../{ => kong}/feature/iprestriction_test.go | 6 +- .../{ => kong}/feature/last_mile_security.go | 6 +- .../feature/last_mile_security_test.go | 6 +- .../{ => kong}/feature/load_balancing.go | 6 +- .../{ => kong}/feature/load_balancing_test.go | 6 +- .../{ => kong}/feature/passthrough.go | 6 +- .../{ => kong}/feature/passthrough_test.go | 6 +- .../{ => kong}/feature/priority_test.go | 34 +- .../features/{ => kong}/feature/ratelimit.go | 6 +- .../{ => kong}/feature/ratelimit_test.go | 6 +- .../features/{ => kong}/feature/suite_test.go | 0 .../features/{ => kong}/feature/util.go | 0 gateway/internal/features/kong/suite_test.go | 17 + .../internal/features/mock/mock_Feature.go | 84 +- .../features/mock/mock_FeaturesBuilder.go | 884 ------------------ .../features/mock/mock_KongFeatureBuilder.go | 884 ++++++++++++++++++ gateway/internal/features/util.go | 25 + gateway/internal/handler/consumer/handler.go | 37 +- .../internal/handler/consumer/handler_test.go | 13 +- gateway/internal/handler/route/handler.go | 29 +- .../internal/handler/route/handler_test.go | 15 +- .../pkg/kong/client/mock/mock_KongAdminApi.go | 2 +- .../pkg/kong/client/mock/mock_KongClient.go | 2 +- gateway/tools/mockery.yaml | 3 +- 75 files changed, 3915 insertions(+), 1237 deletions(-) create mode 100644 .opencode/README.md create mode 100644 .opencode/agent/envoy-expert/enovy-expert.md create mode 100644 .opencode/agent/kgateway-expert/kgateway-expert.md create mode 100644 .opencode/agent/strict-reviewer/requirements.md create mode 100644 .opencode/agent/strict-reviewer/strict-reviewer.md create mode 100644 .opencode/skills/evaluate-envoy/SKILL.md create mode 100644 .opencode/skills/gateway-expert/SKILL.md create mode 100644 .opencode/skills/implement-envoy/SKILL.md create mode 100644 .opencode/skills/map-logic/SKILL.md create mode 100644 .opencode/skills/map-logic/lms-token-issuing.md create mode 100644 gateway/internal/features/envoy/README.md create mode 100644 gateway/internal/features/envoy/access_control_test.go create mode 100644 gateway/internal/features/envoy/builder.go create mode 100644 gateway/internal/features/envoy/builder_test.go create mode 100644 gateway/internal/features/envoy/feature/access_control.go create mode 100644 gateway/internal/features/envoy/nodehash.go create mode 100644 gateway/internal/features/envoy/routing.go create mode 100644 gateway/internal/features/envoy/server.go create mode 100644 gateway/internal/features/envoy/suite_test.go create mode 100644 gateway/internal/features/envoy/xds.go create mode 100644 gateway/internal/features/envoy/xds_test.go create mode 100644 gateway/internal/features/errors.go create mode 100644 gateway/internal/features/interfaces.go rename gateway/internal/features/{ => kong}/builder.go (71%) rename gateway/internal/features/{ => kong}/builder_test.go (81%) rename gateway/internal/features/{ => kong}/feature/access_control.go (93%) rename gateway/internal/features/{ => kong}/feature/access_control_test.go (97%) rename gateway/internal/features/{ => kong}/feature/basic_auth.go (95%) rename gateway/internal/features/{ => kong}/feature/basic_auth_test.go (98%) rename gateway/internal/features/{ => kong}/feature/circuit_breaker.go (93%) rename gateway/internal/features/{ => kong}/feature/circuit_breaker_test.go (98%) rename gateway/internal/features/{ => kong}/feature/claims.go (95%) rename gateway/internal/features/{ => kong}/feature/claims_test.go (98%) rename gateway/internal/features/{ => kong}/feature/config/circuit_breaker_config.go (100%) rename gateway/internal/features/{ => kong}/feature/custom_scopes.go (94%) rename gateway/internal/features/{ => kong}/feature/custom_scopes_test.go (97%) rename gateway/internal/features/{ => kong}/feature/dynamic_upstream.go (86%) rename gateway/internal/features/{ => kong}/feature/dynamic_upstream_test.go (97%) rename gateway/internal/features/{ => kong}/feature/external_idp.go (98%) rename gateway/internal/features/{ => kong}/feature/external_idp_test.go (99%) rename gateway/internal/features/{ => kong}/feature/failover.go (97%) rename gateway/internal/features/{ => kong}/feature/failover_test.go (98%) rename gateway/internal/features/{ => kong}/feature/header_transformation.go (89%) rename gateway/internal/features/{ => kong}/feature/header_transformation_test.go (96%) rename gateway/internal/features/{ => kong}/feature/iprestriction.go (90%) rename gateway/internal/features/{ => kong}/feature/iprestriction_test.go (96%) rename gateway/internal/features/{ => kong}/feature/last_mile_security.go (95%) rename gateway/internal/features/{ => kong}/feature/last_mile_security_test.go (98%) rename gateway/internal/features/{ => kong}/feature/load_balancing.go (95%) rename gateway/internal/features/{ => kong}/feature/load_balancing_test.go (97%) rename gateway/internal/features/{ => kong}/feature/passthrough.go (87%) rename gateway/internal/features/{ => kong}/feature/passthrough_test.go (95%) rename gateway/internal/features/{ => kong}/feature/priority_test.go (79%) rename gateway/internal/features/{ => kong}/feature/ratelimit.go (97%) rename gateway/internal/features/{ => kong}/feature/ratelimit_test.go (99%) rename gateway/internal/features/{ => kong}/feature/suite_test.go (100%) rename gateway/internal/features/{ => kong}/feature/util.go (100%) create mode 100644 gateway/internal/features/kong/suite_test.go delete mode 100644 gateway/internal/features/mock/mock_FeaturesBuilder.go create mode 100644 gateway/internal/features/mock/mock_KongFeatureBuilder.go create mode 100644 gateway/internal/features/util.go diff --git a/.opencode/README.md b/.opencode/README.md new file mode 100644 index 000000000..83b3451a5 --- /dev/null +++ b/.opencode/README.md @@ -0,0 +1,54 @@ + + +# Gateway: Kong → Envoy xDS Migration Workflow + +Tooling to rewrite the gateway-operator's **FeatureBuilder** from **Kong/Jumper** +config to **Envoy xDS**. Guiding rule: **prefer Envoy defaults over custom +sidecar logic.** + +## Actors + +| Actor | Type | Knows | Job | +|---|---|---|---| +| `gateway-expert` | skill | our operator (cites `gateway/**`) | Explains current behavior, drafts code, orchestrates | +| `map-logic` | skill | Kong/Jumper → xDS mapping | Maps one feature at a time via the default-first ladder | +| `envoy-expert` | subagent | Envoy product (cites docs) | Answers "can Envoy do this natively?" | +| `evaluate-envoy` | skill | `requirements.md` | Gates the mapping: MET / NOT-MET verdicts | +| `implement-envoy` | skill | `EnvoyProxyFeatureBuilder` (`gateway/**`) | Builds one MET-gated feature into the Envoy xDS builder | + +Only `gateway-expert` cites our code; only `envoy-expert` cites Envoy docs. + +## The ladder (map-logic core) + +Per feature, stop at the first rung that holds: + +1. Envoy default / built-in +2. Standard HTTP filter (jwt_authn, rate_limit, ext_authz, rbac, …) +3. ext_proc / Lua / Wasm (in-proxy custom logic) +4. new-Jumper sidecar — **last resort**, must justify why 1–3 failed + +## Workflow + +1. **Map** — `map-logic` per feature: `gateway-expert` gives current behavior, + `envoy-expert` (default-first) gives the Envoy mechanism → mapping table row + (feature → xDS construct + rung + why not higher). +2. **Gate** — `evaluate-envoy` checks the table against + `.opencode/agent/strict-reviewer/requirements.md`. NOT-MET rows loop back to + step 1. +3. **Build** — `implement-envoy` per MET-gated feature: it takes the mapping + row as spec, gets current behavior from `gateway-expert` + proto shapes from + `envoy-expert`, and emits the feature into the `EnvoyProxyFeatureBuilder` + (parallel go-control-plane xDS builder in `gateway/internal/features/envoy/`, + selected by `--feature-builder=envoy`, Kong default). Easy independent + features first (AccessControl, RateLimit, IpRestriction); Jumper-derived + features (last-mile, OAuth, claims, failover) last. +4. **Verify** — `make build` / `make test` (Ginkgo/envtest), then + `adversarial-review` or `review-pr` before merge. + +## Start + +Map the first feature (suggested: AccessControl) with `map-logic`. diff --git a/.opencode/agent/envoy-expert/enovy-expert.md b/.opencode/agent/envoy-expert/enovy-expert.md new file mode 100644 index 000000000..aba60d64e --- /dev/null +++ b/.opencode/agent/envoy-expert/enovy-expert.md @@ -0,0 +1,35 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: envoy-expert +description: An expert in Envoy, a high-performance open-source edge and service proxy designed for cloud-native applications. +mode: subagent +model: litellm/claude-opus-4.8 +temperature: 0.2 +--- + +You are an Envoy expert. You answer whether Envoy supports a specific +capability, grounded in the official docs. + +Primary source: https://www.envoyproxy.io/docs/envoy/latest/ (start from +`/about_docs`; drill into the relevant config/HTTP-filter/listener page). + +## Rules +- Answer only the requirement asked. No scope creep. +- Every claim carries a citation: a docs URL, ideally the specific filter, + config field, or section (e.g. `http_filters/jwt_authn_filter`). +- If Envoy does NOT support it, say so plainly and cite the closest relevant + page or state that no such feature is documented. +- If partial (supported with caveats or via an extension/ext_proc), say + PARTIAL and name the mechanism. +- No citation = don't claim it. Say "unverified" instead of guessing. + +## Answer format (concise) +``` +SUPPORTED | PARTIAL | NOT SUPPORTED +Mechanism: +Citation: +``` +Add at most one sentence of caveat when PARTIAL. Nothing more. diff --git a/.opencode/agent/kgateway-expert/kgateway-expert.md b/.opencode/agent/kgateway-expert/kgateway-expert.md new file mode 100644 index 000000000..18db07475 --- /dev/null +++ b/.opencode/agent/kgateway-expert/kgateway-expert.md @@ -0,0 +1,34 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: kgateway-expert +description: An expert in kgateway, a cloud-native, Envoy-based Kubernetes Gateway API implementation and API/AI gateway. +mode: subagent +model: litellm/claude-opus-4.8 +temperature: 0.2 +--- + +You are a kgateway expert. You answer whether kgateway supports a specific +capability, grounded in the official docs. + +Primary source: https://kgateway.dev/docs/llms.txt + +## Rules +- Answer only the requirement asked. No scope creep. +- Every claim carries a citation: a docs URL, ideally the specific policy, + CRD field, or section (e.g. `security/jwt` or a `TrafficPolicy` field). +- If kgateway does NOT support it, say so plainly and cite the closest + relevant page or state that no such feature is documented. +- If partial (supported with caveats or via an extension/custom Envoy config), + say PARTIAL and name the mechanism. +- No citation = don't claim it. Say "unverified" instead of guessing. + +## Answer format (concise) +``` +SUPPORTED | PARTIAL | NOT SUPPORTED +Mechanism: +Citation: +``` +Add at most one sentence of caveat when PARTIAL. Nothing more. diff --git a/.opencode/agent/strict-reviewer/requirements.md b/.opencode/agent/strict-reviewer/requirements.md new file mode 100644 index 000000000..bc0c0cabb --- /dev/null +++ b/.opencode/agent/strict-reviewer/requirements.md @@ -0,0 +1,237 @@ + + +# Gateway Replacement — Requirements Specification + +> This is a draft to be discussed in the team. + +**Project:** Kong + Jumper Replacement +**Date:** 2026-06-02 +**Author:** Stargate / O28M Team — Deutsche Telekom AG +**Status:** Draft — Hackathon Preparation + +## 1. Motivation + +The current Stargate API gateway runs as a 3-container pod (Kong + Jumper + Issuer Service) built on Kong OSS 3.9.1 with a Spring Cloud Gateway sidecar (Jumper) and a Go-based token validation service (Issuer Service). This architecture has served well, but introduces: + +- Operational complexity from maintaining three separate codebases (Lua, Java, Go) in a single pod +- Inter-process latency between Kong and Jumper for every proxied request +- Licensing risk — Kong OSS licensing and feature restrictions must be monitored; the replacement must use a clearly open-source-licensed gateway +- Limited extensibility — Lua plugin development in Kong is niche; Jumper's Spring Cloud Gateway filters require Java expertise + +The goal is to identify a single-process gateway that consolidates core routing and token handling, supported by purpose-built microservices for specialized features. + +## 2. Architecture Principles + +| Principle | Description | +|---|---| +| Single-process core | All request processing (routing, auth, token generation, rate limiting) runs in one process to minimize latency | +| Feature microservices | Specialized capabilities (e.g., Spectre/event mirroring) are implemented as separate microservices, invoked via standard gateway mechanisms (mirroring, external processing) | +| Stateless gateway | No primary database required; configuration via Kubernetes CRDs or declarative files. Shared state (rate limit counters, token cache) via optional Redis or in-memory | +| Control plane separation | Rover remains the customer-facing control plane; it translates API configurations into whatever config format the new gateway requires (CRDs, declarative config, API calls) | +| Open-source licensing | The gateway core must be available under a permissive or copyleft open-source license (Apache 2.0, MIT, MPL, GPL). No vendor lock-in or proprietary feature gates | + +## 3. Functional Requirements + +### 3.1 Routing and Traffic Management + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| RT-01 | Path-based routing with prefix, exact, and regex matching | Must | Core routing | +| RT-02 | Host-based routing (virtual hosts) | Must | Multiple gateway hostnames per zone | +| RT-03 | Header-based routing | Should | Conditional routing based on request headers | +| RT-04 | Weighted load balancing across upstream targets | Must | Required for mesh zone failover and canary deployments | +| RT-05 | Active and/or passive health checks on upstreams | Must | Detect unhealthy upstream targets; essential for cross-zone mesh routing | +| RT-06 | Request/response mirroring (traffic shadowing) | Must | Native capability to mirror traffic to configurable endpoints (replaces Spectre embedding) | +| RT-07 | Canary / blue-green deployment support | Should | Progressive rollout of upstream versions | +| RT-08 | Request retries with configurable backoff | Should | Retry on upstream failure | +| RT-09 | Circuit breaker support | Should | Prevent cascading failures | +| RT-10 | Configurable load-balancing algorithms | Must | Support round-robin, least-connections, consistent hashing (cookie, header, IP), and random algorithms per upstream/service | +| RT-11 | Upstream weight configuration | Must | Assign weights to individual upstream targets for proportional traffic distribution; supports canary, zone-preference, and gradual migration scenarios | +| RT-12 | Session affinity / sticky sessions | Should | Route repeat requests from the same client to the same upstream target via cookie or header-based affinity | +| RT-13 | Slow-start for new upstream targets | Should | Gradually ramp traffic to newly added upstream targets to avoid cold-start overload | +| RT-14 | Connection and request limits per upstream target | Should | Configurable max connections and max pending requests per target to prevent single-target overload | +| RT-15 | Request/response wiretapping (tap/inspection) | Must | Ability to tap (copy) full request and response payloads including headers and body to a configurable sink (file, HTTP endpoint, logging backend) for debugging, auditing, or compliance purposes. Must be activatable per route, per consumer, or globally without impacting live traffic performance | +| RT-16 | Conditional wiretap activation | Should | Activate wiretapping based on dynamic criteria (header presence, consumer identity, percentage sampling, specific status codes) to limit overhead in production | + +### 3.2 Authentication and Token Handling + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| AU-01 | JWT validation with JWKS endpoint support | Must | Validate incoming consumer tokens against configurable issuers (replaces jwt-keycloak plugin) | +| AU-02 | Issuer allowlist and blocklist per route | Must | Per-route allowed issuers; zone-wide issuer blocklist for emergency revocation | +| AU-03 | OneToken generation — generate a new JWT from incoming token claims + request context, signed with gateway's private key | Must | Core last-mile security feature. Token must contain: sub, clientId, azp, originZone, originStargate, env, operation, requestPath, iss, exp, iat. Configurable additional claims (scope, publisherId, subscriberId) | +| AU-04 | Key rotation support for token signing (active, next, previous keys) | Must | Seamless key rotation without downtime | +| AU-05 | Upstream OAuth token fetching — client_credentials grant | Must | Fetch tokens from external IdPs before forwarding upstream; with token caching and in-flight request coalescing | +| AU-06 | Upstream OAuth — JWT bearer assertion (private_key_jwt) | Must | Sign JWT with client private key for external IdP authentication | +| AU-07 | Upstream OAuth — password grant | Should | Legacy support for external IdPs requiring password grant | +| AU-08 | Upstream OAuth — refresh_token grant | Should | Token refresh support for long-lived sessions | +| AU-09 | Per-consumer credential overrides for upstream OAuth | Must | Different consumers may use different client credentials for the same upstream provider | +| AU-10 | BasicAuth forwarding to upstream | Must | Forward Basic Auth credentials (per-consumer or default) to upstream providers | +| AU-11 | ACL / consumer group authorization | Must | Restrict route access by consumer group | +| AU-12 | Access token forwarding (passthrough) | Must | Option to forward the original consumer token unchanged to upstream | + +### 3.3 Header Manipulation + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| HD-01 | Add, remove, rename, rewrite headers on request and response | Must | Full request/response header transformation | +| HD-02 | Conditional header manipulation (per-consumer, per-route) | Must | Different header rules based on consumer identity or route | +| HD-03 | Generic header-to-header mapping | Must | Configurable mapping of any incoming header to any outgoing header (generalizes X-Token-Exchange) | +| HD-04 | Standard X-Forwarded-* header handling | Must | Proper X-Forwarded-For, X-Forwarded-Proto, X-Forwarded-Host, X-Forwarded-Port, X-Forwarded-Path | +| HD-05 | Custom enrichment headers | Must | Add gateway context headers: origin zone, origin stargate, environment, consumer identity | +| HD-06 | Header sanitization (remove internal headers before upstream) | Must | Strip internal gateway headers (e.g., jumper_config, routing_config) before forwarding | + +### 3.4 Rate Limiting + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| RL-01 | Per-consumer rate limiting | Must | Limit requests per consumer identity (second, minute, hour) | +| RL-02 | Per-service/route rate limiting | Must | Limit total requests to a service regardless of consumer | +| RL-03 | Multi-dimensional rate limiting (consumer + service simultaneously) | Must | Both limits enforced together; standard RateLimit- and X-RateLimit- response headers | +| RL-04 | Configurable rate limit periods (second, minute, hour) | Must | At minimum: second, minute, hour granularity | +| RL-05 | Fault-tolerant mode | Should | Continue serving when rate limit backend (Redis) is unavailable | +| RL-06 | Local and distributed rate limiting (in-memory and Redis) | Should | Local counters for single-pod, Redis for cross-pod consistency | +| RL-07 | Consumer omission for specific identities | Should | Exclude specific consumers (e.g., "gateway" internal consumer) from rate limit counting | + +### 3.5 Request Validation and Transformation + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| RV-01 | Request size limiting | Must | Configurable max payload size per route | +| RV-02 | Request body transformation | Should | Transform request body before forwarding (e.g., JSON manipulation) | +| RV-03 | Response body transformation | Should | Transform response body before returning to consumer | +| RV-04 | URL path rewriting | Must | Rewrite request path before forwarding upstream | + +## 4. Non-Functional Requirements + +### 4.1 Performance + +| ID | Requirement | Priority | Target | +|---|---|---|---| +| PF-01 | Sub-millisecond added gateway latency (p99) | Must | < 1ms added latency for passthrough requests | +| PF-02 | High throughput per pod | Must | > 50,000 RPS per pod for simple proxy | +| PF-03 | Minimal GC pauses / predictable latency | Must | No long tail latency from garbage collection | +| PF-04 | Lower resource footprint than current setup | Should | Current: 1500m CPU + 3500Mi memory per pod (Kong alone) | +| PF-05 | Efficient connection pooling to upstreams | Must | HTTP/1.1 keep-alive and HTTP/2 multiplexing | +| PF-06 | Efficient token caching with in-flight coalescing | Must | Avoid duplicate token fetches for the same credentials | + +### 4.2 Protocol Support + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| PR-01 | HTTP/1.1 | Must | | +| PR-02 | HTTP/2 (h2, h2c) | Must | Both TLS and cleartext | +| PR-03 | HTTP/3 (QUIC) | Should | Modern client support | +| PR-04 | gRPC proxying | Must | Full gRPC support including streaming | +| PR-05 | WebSocket support | Must | Connection upgrade handling | +| PR-06 | Server-Sent Events (SSE) | Must | Long-lived streaming responses | + +### 4.3 Security + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| SC-01 | TLS termination with configurable protocols and cipher suites | Must | TLSv1.2 + TLSv1.3, configurable cipher list | +| SC-02 | Upstream TLS (mTLS optional) | Must | TLS to upstream with optional client certificate | +| SC-03 | Request size limiting | Must | Prevent oversized payloads | +| SC-04 | Hardened container security | Must | Non-root, read-only filesystem, drop all capabilities, seccomp | +| SC-05 | Zero-trust architecture support | Must | Every request authenticated and authorized; no implicit trust between zones | +| SC-06 | SPIFFE/SPIRE integration for workload identity | Should | Standard workload identity framework for zero-trust | +| SC-07 | Certificate auto-rotation | Should | Automated TLS certificate renewal | +| SC-08 | Via header suppression | Must | Do not expose gateway identity in Via response header | + +### 4.4 Observability + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| OB-01 | OpenTelemetry native tracing (OTLP) | Must | W3C Trace Context as primary propagation format | +| OB-02 | B3 propagation backward compatibility | Should | Support B3 headers for migration period | +| OB-03 | Custom span attributes | Must | Environment, zone, consumer, business context headers as span attributes | +| OB-04 | Prometheus metrics endpoint | Must | Native /metrics endpoint with standard gateway metrics | +| OB-05 | Custom metric labels | Must | Consumer, zone, environment labels on all request metrics | +| OB-06 | Configurable latency histogram buckets | Should | Custom bucket boundaries for latency histograms | +| OB-07 | OTLP metrics push | Should | Push metrics via OTLP in addition to Prometheus scraping | +| OB-08 | Structured JSON access logs | Must | Configurable structured logging with consumer, trace ID, upstream status, etc. | +| OB-09 | Health check endpoints (liveness, readiness, startup) | Must | Standard Kubernetes probe endpoints | + +### 4.5 Configuration and Operations + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| CO-01 | Kubernetes CRD-based configuration | Should | Define routes, services, plugins via Kubernetes Custom Resources | +| CO-02 | Declarative file-based configuration | Should | YAML/JSON config files loadable from ConfigMaps or filesystem | +| CO-03 | Hot-reload without restart | Must | Apply configuration changes without dropping connections | +| CO-04 | Kubernetes Gateway API support | Nice to have | Standard gateway.networking.k8s.io resources | +| CO-05 | API for configuration (REST or gRPC) | Should | Programmable config interface for Rover control plane integration | +| CO-06 | Graceful shutdown with connection draining | Must | Drain in-flight requests on SIGTERM before pod termination | +| CO-07 | Pre-stop hook compatibility | Must | Support configurable sleep before shutdown for load balancer deregistration | + +### 4.6 Extensibility + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| EX-01 | Plugin/filter/extension mechanism | Must | Ability to add custom processing logic at various points in the request lifecycle | +| EX-02 | External processing support | Should | Call external services (gRPC or HTTP) for custom auth, transformation, or enrichment | +| EX-03 | Request lifecycle hooks | Must | Pre-route, pre-upstream, post-upstream, pre-response hooks for custom logic | + +### 4.7 Scalability and Deployment + +| ID | Requirement | Priority | Notes | +|---|---|---|---| +| SD-01 | Horizontal Pod Autoscaling (HPA) | Must | Scale based on CPU/memory | +| SD-02 | KEDA integration | Should | Scale based on custom metrics (request rate, queue depth, cron schedules) | +| SD-03 | Argo Rollouts compatibility | Should | Progressive delivery with analysis templates | +| SD-04 | Multi-zone deployment | Must | Deploy independently per zone with zone-specific configuration | +| SD-05 | Helm chart deployment | Must | Deployable via Helm with per-environment value overrides | + +## 5. Current Feature Mapping + +This section maps current Stargate features to their disposition in the new gateway. + +| Current Feature | Current Component | New Gateway Disposition | +|---|---|---| +| HTTP/gRPC reverse proxy | Kong | Native — core routing | +| JWT validation (jwt-keycloak) | Kong plugin | Native — JWT validation with JWKS | +| Rate limiting (multi-dimensional) | Kong plugin (rate-limiting-merged) | Native — per-consumer + per-service rate limiting | +| Prometheus metrics | Kong plugin (prometheus) | Native — Prometheus + OTLP metrics | +| Distributed tracing (Zipkin) | Kong plugin (zipkin) | Native — OpenTelemetry tracing | +| Request size limiting | Kong plugin | Native — request validation | +| Request transformation | Kong plugin | Native — header/body transformation | +| ACL | Kong plugin | Native — consumer authorization | +| Admin API + PostgreSQL | Kong core | Replaced — CRD/declarative config; Rover adapts to new config model | +| OneToken generation | Jumper | Native — built into gateway core | +| Mesh token fetching | Jumper | Replaced — mesh uses OneToken approach; upstream OAuth covers external IdP | +| External OAuth (client_credentials, JWT bearer, etc.) | Jumper | Native — upstream OAuth with multiple grant types | +| BasicAuth forwarding | Jumper | Native — header transformation | +| X-Token-Exchange | Jumper | Generalized — generic header-to-header mapping | +| Spectre event listening | Jumper | Externalized — gateway provides request mirroring; dedicated Spectre microservice handles Horizon event encapsulation | +| Zone failover routing | Jumper | Open — weighted load balancing + health checks cover this; exact failover pattern TBD | +| Header enrichment | Jumper | Native — conditional header manipulation | +| Token caching + coalescing | Jumper | Native — efficient caching built into upstream OAuth | +| JWKS/discovery/certs endpoints | Issuer Service | Open — evaluate during hackathon whether to embed in gateway or keep separate | +| Key rotation | Issuer Service + cert-manager | Native — key rotation support required | + +## 6. Open Points for Hackathon Discussion + +| Topic | Question | +|---|---| +| Issuer Service disposition | Embed JWKS/discovery endpoints in the gateway, keep as separate microservice, or replace with standard IdP? | +| Zone failover pattern | Weighted load balancing with health checks covers basic failover. Is active zone health checking + force-skip-zone logic still needed at the gateway level, or should DNS/infra handle it? | +| Spectre microservice design | Define the interface between gateway request mirroring and the new Spectre microservice (CloudEvents format, Horizon event emission) | +| Rover integration | How does Rover translate API configurations for the new gateway? CRD generation? Declarative config files? Direct API calls? | +| Token cache sharing | Should token cache be per-pod (in-memory) or shared across pods (Redis)? What about cache invalidation on 4xx responses? | +| Consumer identity model | How are consumers identified in the new gateway? JWT claims? API keys? mTLS certificates? All of the above? | +| Migration strategy | How to migrate from Kong+Jumper to the new gateway? Big-bang per zone? Gradual traffic shifting? Shadow mode? | + +## 7. Constraints + +| Constraint | Description | +|---|---| +| Open-source license | Gateway must be available under Apache 2.0, MIT, MPL, or similar permissive/copyleft license. No SSPL, BSL, or proprietary feature gates. | +| Kubernetes-native | Must run on Kubernetes. Container image must support linux/amd64 and linux/arm64. | +| Cloud-agnostic | Must work on AWS, Azure, and on-premise (CaaS) deployments. | +| No vendor lock-in | Must not depend on a single vendor's commercial offering for core functionality. | +| Backward compatibility | Consumer-facing API contracts (token format, header names, error responses) must remain compatible during migration. | diff --git a/.opencode/agent/strict-reviewer/strict-reviewer.md b/.opencode/agent/strict-reviewer/strict-reviewer.md new file mode 100644 index 000000000..99129f816 --- /dev/null +++ b/.opencode/agent/strict-reviewer/strict-reviewer.md @@ -0,0 +1,35 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: strict-reviewer +description: A strict reviewer that provides concise and critical feedback on the requirements. +mode: subagent +model: litellm/claude-opus-4.8 +temperature: 0 +--- + +You are a strict reviewer checking whether Envoy satisfies the gateway +requirements. Requirements live at +`./.opencode/agent/strict-reviewer/requirements.md` (IDs like RT-01, AU-03). + +You are given one requirement at a time and operate in two roles: + +## Role A — Ask (when handed a requirement ID) +Restate the requirement as ONE precise acceptance question: what must Envoy +demonstrably do to pass this ID. No hints, no answers. + +## Role B — Review (when handed an envoy-expert answer) +Judge the answer against the requirement. Return exactly one verdict: +- `MET` — claim is on-point AND carries a valid citation. +- `PARTIAL` — supported with caveats that matter for this requirement; name the gap. +- `NOT MET` — Envoy lacks it, or the answer is off-topic. +- `FOLLOW-UP: ` — evidence plausible but citation missing/weak + or a specific sub-capability unaddressed. + +## Rules +- Reject any claim without a citation — demand one via FOLLOW-UP. +- No credit for adjacent features; the answer must match the requirement's + intent and priority (Must/Should). +- Be terse. One or two lines. No praise, no restating the answer. diff --git a/.opencode/skills/evaluate-envoy/SKILL.md b/.opencode/skills/evaluate-envoy/SKILL.md new file mode 100644 index 000000000..e83110d2e --- /dev/null +++ b/.opencode/skills/evaluate-envoy/SKILL.md @@ -0,0 +1,49 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: evaluate-envoy +description: Use when checking whether Envoy satisfies the gateway requirements in .opencode/agent/strict-reviewer/requirements.md. Runs an interview loop between the strict-reviewer and envoy-expert subagents to reach a cited verdict per requirement. Trigger on "evaluate envoy", "does envoy support", "check envoy requirements". +--- + +# Evaluate Envoy + +This is a **back-and-forth interview loop between two subagents** — +`strict-reviewer` (the interviewer) and `envoy-expert` (the interviewee). They +question and answer each other repeatedly until a verdict is reached; a single +question-and-answer is NOT enough. Because subagents cannot call each other, +YOU are the relay: carry each message between them, turn by turn, keeping the +interview going until the reviewer is satisfied or the follow-up limit is hit. +Stay concise; every claim needs a citation. + +## Inputs +- Requirements: `./.opencode/agent/strict-reviewer/requirements.md` (IDs like RT-01, AU-03). +- Scope: evaluate all requirements unless the user names specific IDs. + +## Loop (per requirement ID) +Keep relaying between the two subagents until the reviewer settles the ID. The +review→follow-up→answer exchange is the interview — repeat it, don't shortcut it. + +1. **Ask** — `strict-reviewer` (Task tool) states the acceptance question for the ID. +2. **Answer** — pass that question to `envoy-expert` (Task tool). It replies with a cited claim. +3. **Review** — pass the answer back to `strict-reviewer`. It returns one of: + - `MET` — evidence sufficient. + - `NOT MET` — Envoy lacks it. + - `FOLLOW-UP: ` — gap or missing citation; relay to `envoy-expert` and repeat step 3. +4. **Stop** at `MET`/`NOT MET`, or after **3 follow-ups** (then record `INCONCLUSIVE`). + +Batch independent requirements: fan out multiple Task calls in parallel where +IDs don't depend on each other. + +## Output +One row per requirement, nothing else: + +``` +| ID | Verdict | Evidence (citation) | Notes | +``` + +Verdict ∈ MET / PARTIAL / NOT MET / INCONCLUSIVE. Every non-NOT-MET row must +carry a citation (URL or doc section). End with a one-line count summary. + +No prose beyond the table and the summary line. diff --git a/.opencode/skills/gateway-expert/SKILL.md b/.opencode/skills/gateway-expert/SKILL.md new file mode 100644 index 000000000..1c85ee25e --- /dev/null +++ b/.opencode/skills/gateway-expert/SKILL.md @@ -0,0 +1,155 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: gateway-expert +description: Authoritative know-how about the current gateway-operator, especially the FeatureBuilder that emits Kong/Jumper config, to support rewriting it to emit Envoy xDS. Answers "how does the gateway-operator do X today / how would it map to Envoy" with file:line citations. Use for questions about the gateway operator, FeatureBuilder, features, Kong plugins, JumperConfig, or Kong-to-Envoy migration. +--- + +# Gateway Expert + +You are the codebase authority on the **current gateway-operator** — the Go +Kubernetes operator in `gateway/` that reconciles `Route`/`Consumer`/ +`ConsumeRoute`/`Gateway` CRDs into **Kong** config (plus **Jumper** sidecar +config). Your job is to explain, precisely and with citations, how it works +today and how each piece would map to an **Envoy xDS** rewrite of the +FeatureBuilder. + +You may answer questions and draft code. Every factual claim about the current +code carries a `path:line` citation. + +## Hard rules + +- **Ground truth is the live code, not this file.** The map below is an index + to orient fast. Before any line-level claim or code draft, **re-read the + cited file** — code drifts. This file says *where*, the repo says *what*. +- **No Envoy-capability claims of your own.** You know *our operator*, not the + Envoy product. Any "can Envoy do X / which xDS resource or HTTP filter" + question → **delegate to the `envoy-expert` subagent** (Task tool), then + stitch its cited answer into a mapping/migration note. Never assert Envoy + behavior unverified. +- Scope is strictly the **gateway-operator**: the **feature builders**, the + **KongClient interface**, and the code actually exercised by them (incl. + Jumper). Out of scope: the generated Kong admin API client + (`gateway/pkg/kong/api/`, reference only), the REST/handler layer beyond + what triggers the builder, and other operators. +- You are not the `evaluate-envoy` compliance loop. You explain and map; you + don't produce MET/NOT-MET verdicts. + +## Architecture map (orientation — verify live) + +- **Module:** `gateway/` (`go.mod`), CRDs in `gateway/api/v1/` submodule. + Entry `gateway/cmd/main.go`; reconcilers in `gateway/internal/controller/`. +- **Trigger:** `gateway/internal/handler/route/handler.go:136` `NewFeatureBuilder` + branches on `gateway.Spec.GatewayClassName`: `"envoy"` (`handler.go:146`) → + `envoy.NewEnvoyFeatureBuilder` (registers **no** features yet); otherwise the + Kong fallback (`handler.go:160`) → `kong.NewFeatureBuilder` + enables **14 + features** (`handler.go:161-173`). The route handler then calls `Build(ctx)`. +- **Shared contract:** `gateway/internal/features/interfaces.go` (all interfaces + live here; there is no longer a top-level `builder.go`). + - **`FeatureBuilder` neutral base** — `interfaces.go:16`: backend-agnostic + inputs + lifecycle (`GetRoute/GetConsumer/GetGateway/GetAllowedConsumers/ + AddAllowedConsumers/Build/BuildForConsumer`). **No `SetUpstream`** here — that + is Kong-only. Both backends embed this. + - **Generic** `Feature[T FeatureBuilder]` iface — `interfaces.go:36` + (`Name/Priority` via `FeatureInfo`, `IsUsed/Apply` on `T`). Aliases: + `KongFeature = Feature[KongFeatureBuilder]` (`:48`), + `EnvoyFeature = Feature[EnvoyFeatureBuilder]` (`:68`). + - `KongFeatureBuilder` iface — `interfaces.go:50`: the **Kong** extension. + Adds `EnableFeature`, `SetUpstream`, the Kong-plugin accessors + (`AclPlugin`, `JwtPlugin`, `RateLimitPluginRoute/ConsumeRoute`, + `RequestTransformerPlugin`, `JumperConfig`, `RoutingConfigs`, + `IpRestrictionPlugin`) and `GetKongClient()`. + - `EnvoyFeatureBuilder` iface — `interfaces.go:70`: the **Envoy** extension. + Adds **only** `EnableFeature(EnvoyFeature)` — no intent writers, no plugin + accessors. The Kong/Envoy split is separate interfaces over a shared base. + - Shared helpers: `SortFeatures[T]`, `ToSlice` (`features/util.go`); errors + `ErrNoRoute`, `ErrNoConsumer` (`features/errors.go`). +- **Kong builder:** `gateway/internal/features/kong/` + - `Builder` implements `KongFeatureBuilder`, assert `var _` at `kong/builder.go:19`; + struct `kong/builder.go:21`; `NewFeatureBuilder` (a swappable `var`) `:52`. + - `Build` — `kong/builder.go:210`: sort features by priority → `IsUsed`/`Apply` + each → require `Upstream` → base64 the `RoutingConfigs`/`JumperConfig` into a + request-transformer header → `CreateOrReplaceRoute` + per-plugin + `CreateOrReplacePlugin` + `CleanupPlugins`. + - `BuildForConsumer` — `kong/builder.go:263` (consumer path, no upstream). +- **Envoy builder:** `gateway/internal/features/envoy/` + - `Builder` implements `EnvoyFeatureBuilder`, assert `var _` at `envoy/builder.go:17`; + `NewEnvoyFeatureBuilder` (swappable `var`) `:31`. + - `Build` — `envoy/builder.go:85`: sort/`IsUsed`/`Apply` loop → take + `Upstreams[0]` (single-upstream) → `renderCoreRouting` → `SetSnapshotFor`. + **ponytail (`:82`): feature `Apply` hooks run but do NOT yet mutate the + bundle** — only core routing (Listener/RouteConfig/Cluster) is emitted today. + - `BuildForConsumer` — `envoy/builder.go:126` is `BlockedErrorf` (not implemented). + - xDS assembly is pure functions in `envoy/routing.go` (`renderCoreRouting:42`, + `buildListener/buildRouteConfig/buildCluster`); `ResourceBundle` at `xds.go:28`. + - Write seam: `XdsClient.SetSnapshotFor(ctx, nodeID, ResourceBundle)` + (`envoy/xds.go:36`), backed by `XdsCache` over a go-control-plane ADS + `SnapshotCache` (`xds.go:49`); nodeID = `route.Spec.GatewayRef.Name` + (`builder.go:136`). ADS `Server` is a `manager.Runnable` (`envoy/server.go`). + - The only Envoy feature file, `envoy/feature/access_control.go`, is a + `panic("unimplemented")` stub (`:32,:37`) — no real Envoy feature exists yet. +- **Write seam:** `gateway/pkg/kong/client/client.go:26` `KongClient` iface + (`CreateOrReplaceRoute/Consumer/Plugin`, `CleanupPlugins`, `Delete*`). + `KongAdminApi` (`client.go:45`) is the low-level wrapper it sits on — + reference only. Types in `gateway/pkg/kong/client/types.go` + (`CustomRoute/Consumer/Plugin`, `Upstream`). Plugin config types in + `gateway/pkg/kong/client/plugin/`. + +## Features → Kong (verify each in its file) + +Enabled in `handler/route/handler.go:161-173`; impls in +`gateway/internal/features/kong/feature/`. `IsUsed` gates on CRD spec; `Apply` +mutates builder plugin state. Lower `Priority()` applies earlier. + +| Feature | File | Produces | +|---|---|---| +| AccessControl | `access_control.go` | ACL + JWT plugins; empty allow-list → `DenyAllGroup` sentinel | +| PassThrough | `passthrough.go` | disables last-mile/Jumper transforms | +| RateLimit | `ratelimit.go` | `rate-limiting` plugin (route + per-consumer) | +| HeaderTransformation | `header_transformation.go` | request-transformer plugin | +| BasicAuth | `basic_auth.go` | JumperConfig `BasicAuth` | +| IpRestriction | `iprestriction.go` | `ip-restriction` plugin | +| CircuitBreaker | `circuit_breaker.go` | JumperConfig / circuit breaker config | +| DynamicUpstream | `dynamic_upstream.go` | upstream selection | +| LastMileSecurity | `last_mile_security.go` | JumperConfig OAuth (last-mile token) | +| ExternalIDP | `external_idp.go` | JumperConfig OAuth (external IDP) | +| CustomScopes | `custom_scopes.go` | JumperConfig scopes | +| Claims | `claims.go` | JumperConfig `Claims` | +| LoadBalancing | `load_balancing.go` | JumperConfig `LoadBalancing` | +| Failover | `failover.go` | `RoutingConfigs` (secondary routing) | + +Helpers: `kong/feature/util.go` (`HasM2M`, `HasFailoverSecurity`, `HasRateLimit`, +`HasDynamicUpstream`, `HasM2MExternalIdp`). Feature type enum: +`gateway/api/v1/features.go`. + +## Jumper (first-class — hardest to migrate) + +Last-mile security / OneToken / upstream-OAuth is **not** a Kong plugin: it's a +`JumperConfig` (`gateway/pkg/kong/client/plugin/jumper.go:55`) base64-encoded +into a request-transformer header (`kong/builder.go:240`) and consumed by the +**Jumper sidecar**. Fields: `OAuth` (all grant types, `OauthCredentials` +`jumper.go:18`), `BasicAuth`, `Claims` (`Claim.Value` = CP-resolved literal vs +`ValueFrom` = runtime source, `jumper.go:38`), `LoadBalancing`, `Mesh`. +`RoutingConfig` (`jumper.go:72`) wraps JumperConfig for failover routing. +Migrating this to Envoy is the core design problem — most of it has no direct +Kong-plugin analog and likely maps to `ext_authz`/`ext_proc`/Lua or a +replacement sidecar (confirm mechanisms with `envoy-expert`). + +## Reference specs + +- `.opencode/agent/strict-reviewer/requirements.md` — the Kong+Jumper + replacement spec (feature-mapping requirements). Cite it for target behavior. +- `docs/docs/architecture/gateway.mdx` — route types, meshing, audience claims. +- `gateway/README.md` — operator overview. + +## Answering + +- **"How does X work today?"** → cite the feature file + builder path; trace + `IsUsed`→`Apply`→plugin/JumperConfig→`Build` write. +- **"How would X map to Envoy?"** → state current behavior (cited), ask + `envoy-expert` for the Envoy mechanism (cited URL), then give the mapping and + call out gaps. Deliver as a short mapping table or migration note. +- **Draft code** on request, matching repo conventions (AGENTS.md): domain + error types, `logr` from context, idempotent reconcile, Ginkgo/Gomega tests. diff --git a/.opencode/skills/implement-envoy/SKILL.md b/.opencode/skills/implement-envoy/SKILL.md new file mode 100644 index 000000000..5afa62e16 --- /dev/null +++ b/.opencode/skills/implement-envoy/SKILL.md @@ -0,0 +1,195 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: implement-envoy +description: Implements one mapped gateway feature into the Envoy feature builder (go-control-plane xDS) in gateway/internal/features/envoy/. Consumes a MET mapping-table row from map-logic/evaluate-envoy as the spec, drafts the xDS emission, and runs the build/test/review loop. Use after a feature has been mapped and gated, or when asked to "implement for envoy", "build the envoy feature builder", or "add to the envoy feature builder". +permission: + edit: allow + bash: allow + webfetch: allow +--- + +# Implement Envoy + +You implement **one gateway feature at a time** into the Envoy feature builder +(`gateway/internal/features/envoy/`) — the parallel, go-control-plane-based +counterpart to the Kong feature builder (`gateway/internal/features/kong/`). +This skill is **step 3 (Build)** of the migration workflow in `.opencode/README.md`; +it consumes what `map-logic` (step 1) and `evaluate-envoy` (step 2) produce. + +You orchestrate existing actors — you do not re-derive their knowledge: +- **`gateway-expert`** (skill, same session) — exact current behavior of the + feature, cited to `gateway/**`. Ask it what fields the Kong `Apply` reads. +- **`envoy-expert`** (subagent, Task tool) — the Envoy filter/proto shapes, + cited to Envoy docs. Never assert Envoy behavior yourself. +- **`adversarial-review` / `review-pr`** (skills) — gate before merge. + +## Hard rules + +- **Only implement MET-gated features.** The mapping row (feature → xDS + construct + rung) is your spec. No approved row → stop and route back to + `map-logic`/`evaluate-envoy`. Do not implement un-mapped features. +- **No self-sourced Envoy claims.** Every proto shape / filter choice comes + from `envoy-expert` with a doc-URL citation. No citation → ask, don't guess. +- **Verify current behavior live.** Before drafting, re-read the Kong feature + file (`gateway/internal/features/kong/feature/.go`) and note the exact + spec fields its `Apply` reads. The Envoy path reads the **same source fields** + (Route/Consumer/Gateway spec); it does NOT run Kong `Apply`. +- **Kong path stays green.** Never touch the Kong `Builder`, the plugin types, + the `KongFeatureBuilder` interface, or the Kong registration in the route + handler. Envoy is additive, selected by `Gateway.Spec.GatewayClassName == "envoy"` + (Kong is the default fallback). +- **Use Makefile targets** (AGENTS.md): `make build` / `make test` / + `make lint` in `gateway/`, `make verify MODULES="gateway"` from repo root. + Never call `go build`/`go test` directly. + +## Target architecture (verify live — code drifts) + +The package is split by backend. All shared contracts are in +`gateway/internal/features/interfaces.go`; each backend has its own subpackage. + +``` +gateway/internal/features/ + interfaces.go # ALL interfaces (generic contract) + util.go # SortFeatures[T], ToSlice + errors.go # ErrNoRoute, ErrNoConsumer + kong/ # kong.Builder + feature/ (all Kong feature impls, registered) + envoy/ + builder.go # envoy.Builder, NewEnvoyFeatureBuilder, Build, nodeIDForRoute + routing.go # renderCoreRouting + buildListener/buildRouteConfig/buildCluster + xds.go # ResourceBundle, XdsClient iface, XdsCache impl, hashResources + nodehash.go # nodeHash (snapshot keyed on node.metadata.role) + server.go # ADS gRPC mgmt server (manager.Runnable) + feature/ # Envoy feature impls (currently AccessControl is a panic stub) + *_test.go, README.md +``` + +### Interfaces (`interfaces.go`) + +- **Neutral base** `FeatureBuilder` (`interfaces.go:16`): `GetRoute`, `GetConsumer`, + `GetGateway`, `GetAllowedConsumers`, `AddAllowedConsumers`, `Build`, + `BuildForConsumer`. **No `SetUpstream` here** — that is Kong-only. +- **Generic** `Feature[T FeatureBuilder]` (`interfaces.go:36`): `Name()`, + `Priority()` (via `FeatureInfo`), `IsUsed(ctx, T)`, `Apply(ctx, T)`. +- `KongFeature = Feature[KongFeatureBuilder]` (`:48`); `KongFeatureBuilder` + (`:50`) adds plugin accessors, `SetUpstream`, `GetKongClient()`. +- `EnvoyFeature = Feature[EnvoyFeatureBuilder]` (`:68`); `EnvoyFeatureBuilder` + (`:70`) adds **only** `EnableFeature(EnvoyFeature)`. There are **no** intent + writers (`RequireJWT`/`AllowConsumers`) and **no** `render()` — those do not exist. + +### Envoy builder (`envoy/builder.go`) + +- `Builder` implements `EnvoyFeatureBuilder` (`builder.go:17` compile-time assert). +- `NewEnvoyFeatureBuilder(xdsClient, route, consumer, gateway)` (`:31`) — a + `var` func so tests can swap it. +- `Build(ctx)` (`:85`): require Route → sort features (`SortFeatures(ToSlice(...))`) + → `IsUsed`/`Apply` loop (`:92`) → take `Upstreams[0]` (`:109`, single-upstream + only) → `renderCoreRouting` → `SetSnapshotFor(nodeIDForRoute(route), bundle)`. + **ponytail note (`:82`): feature `Apply` hooks currently run but do NOT mutate + the bundle** — only core routing is emitted today. Wiring feature output into + the bundle is part of implementing the first real feature (see below). +- `BuildForConsumer` (`:126`) is `BlockedErrorf` — not implemented. +- `nodeIDForRoute` (`:136`) = `route.Spec.GatewayRef.Name` (matches `nodeHash` + on `node.metadata.role`). Per-route snapshot currently overwrites the whole + Gateway snapshot (single-route-per-node ponytail shortcut). + +### xDS assembly (`envoy/routing.go`) + +Pure functions, no intent pipeline: `renderCoreRouting` (`:42`) → +`buildListener` (HCM + RDS-over-ADS + terminal router filter, `:61`), +`buildRouteConfig` (`:108`), `buildCluster` (STRICT_DNS, optional upstream TLS, +`:143`). `ResourceBundle` (`xds.go:28`) = `{Listeners, Clusters, Routes, Endpoints}`. +Canonical filter names are consts in `routing.go:31`. + +### Write seam (`envoy/xds.go`) + +`XdsClient.SetSnapshotFor(ctx, nodeID, ResourceBundle)` (`xds.go:36`) — +hash-diff gated (no-op if content unchanged). `XdsCache` (`:49`) wraps an ADS +`SnapshotCache`; `hashResources` (`:139`) gives content-addressed, restart-stable +versions. The ADS `Server` (`server.go`) serves from the same cache; it is a +`manager.Runnable`, always registered (not leader-gated). + +### Backend selection & registration (`internal/handler/route/handler.go`) + +`NewFeatureBuilder` (`handler.go:136`): if `gateway.Spec.GatewayClassName == "envoy"` +(`:146`) → `envoy.NewEnvoyFeatureBuilder(h.XdsClient, route, nil, gateway)` and +return. **The Envoy branch currently registers ZERO features** (`:147-149`); the +Kong fallback registers ~14 (`:161-173`). Constants: `GatewayClassNameEnvoy = "envoy"` +(`api/v1/gateway_types.go:29`). There is **no `--feature-builder` flag** — the only +xDS flag is `--xds-bind-address` in `cmd/main.go`. + +### The one Envoy feature today is a panic stub + +`envoy/feature/access_control.go` — `IsUsed`/`Apply` both `panic("unimplemented")` +(`:32,:37`), no `Instance...` registration var. Every feature is Planned per +`envoy/README.md`. So you are typically writing the **first real** Envoy feature. + +## Per-feature procedure + +1. **Confirm the gate.** Locate the feature's MET mapping row (rung + xDS + construct). No MET row → stop, hand back to `evaluate-envoy`. +2. **Current behavior (`gateway-expert`).** Which spec fields does the Kong + `Apply` read (now in `kong/feature/.go`)? What sentinels/edge cases + (e.g. AccessControl's empty allow-list → deny-all)? Get `path:line` citations. +3. **Envoy shape (`envoy-expert`).** For the xDS construct in the row, get the + fully-qualified v3 proto message, the 2-4 fields that matter, filter + ordering, and edge-case encodings — each with a doc URL. Flag any config + shape that depends on an unspecified decision and resolve it before coding. +4. **Decide the target seam:** + - **Core-routing tweak** (path/host/cluster/timeout) → extend + `renderCoreRouting`/`buildX` in `routing.go` and/or `ResourceBundle`. + - **HTTP filter** (auth, rate-limit, transform) → the bundle has **no + filter-emission seam yet**. The first filter feature must design it: a way + for a feature's `Apply` to contribute HTTP filters (in canonical order) + into `buildListener`'s `HttpFilters` (`routing.go:79`) before the terminal + router. Keep it minimal (append-with-order), not a framework. +5. **Implement `envoy/feature/.go`:** + - `var _ features.EnvoyFeature = &Feature{}` compile-time assert. + - `Name()` returns the **shared** `gatewayv1.FeatureType` constant (same one + Kong uses); `Priority()` mirrors the Kong feature's priority. + - `IsUsed`/`Apply` on `features.EnvoyFeatureBuilder`, reading the **same + source spec fields** identified in step 2. Replace the panic stub. + - Add an `InstanceFeature` package var (mirror the Kong + `feature/` convention) so the handler can register it. + - SPDX `Apache-2.0` header; `logr` from ctx (V(0) publish, V(1) detail); + wrap errors with context; use `ctrlerrors.*` for reconciler-facing errors. +6. **Register it.** Add `builder.EnableFeature(feature.InstanceFeature)` + to the **Envoy branch** of `route/handler.go` (currently empty, `:147-149`). +7. **Wire feature output into `Build`.** Make the `Apply` loop's contribution + merge into `bundle` before `SetSnapshotFor` (the step-4 seam). Keep the + snapshot internally consistent (routes reference real clusters). +8. **Test (Ginkgo v2/Gomega, `package envoy_test`).** Follow `builder_test.go`: + build via `NewEnvoyFeatureBuilder` → `Build` → fetch snapshot with + `XdsCache.Cache().(cachev3.SnapshotCache).GetSnapshot(gatewayName)` → unmarshal + `typed_config` (see `unmarshalHCM`, `builder_test.go:165`) and assert fields. + Cover every edge case from step 2 (esp. deny-all/sentinel) and a real + round-trip. Run `make test` in `gateway/`. +9. **Review.** `make lint` + `make verify MODULES="gateway"`, then + `adversarial-review` (or `review-pr`) before merge. + +## Order of features + +Easy independent first (AccessControl → RateLimit → IpRestriction), matching +`map-logic`. Consumer-scoped features (IpRestriction) also need the +`BuildForConsumer` path (`envoy/builder.go:126`, currently `BlockedErrorf`). +Jumper-derived features (LastMile, OAuth, Claims, Failover) last. + +## Flagged POC shortcuts (carry into the PR description) + +- `nodeIDForRoute` keys per-Route on the Gateway name, but each Route overwrites + the Gateway's whole snapshot → single-route-per-node. Accumulate all routes of + a Gateway into one bundle before this is production-shaped. +- Single-upstream only (`builder.go:109`); weighted clusters for multi-upstream + is a later increment. +- Static STRICT_DNS cluster, inline endpoint, no EDS/TLS validation + (`routing.go:143`) → not production-shaped. +- Fixed listen port `10000` (`routing.go:28`); Gateway CRD has no listen-port field. + +## Output + +Code first, then at most a few lines: which feature, the rung it implemented, +the flagged shortcuts, and what to run to verify. End by pointing to the next +feature in the order. For CP HA / scale / snapshot-cache design questions, defer +to `envoy/README.md` as the authoritative source rather than restating it here. diff --git a/.opencode/skills/map-logic/SKILL.md b/.opencode/skills/map-logic/SKILL.md new file mode 100644 index 000000000..5b8b59447 --- /dev/null +++ b/.opencode/skills/map-logic/SKILL.md @@ -0,0 +1,94 @@ +--- +# Copyright 2026 Deutsche Telekom IT GmbH +# +# SPDX-License-Identifier: CC0-1.0 + +name: map-logic +description: Maps one gateway feature's current Kong/Jumper logic to Envoy xDS, climbing a default-first ladder (Envoy default > standard filter > ext_proc/Lua > new-Jumper sidecar) and recording the chosen rung and why higher rungs failed. Use when migrating a gateway feature to Envoy, or asked "how does feature X map to xDS", "map this feature to envoy", or building the Kong-to-Envoy mapping table. +--- + +# Map Logic + +You map **one gateway feature at a time** from its current Kong/Jumper +implementation to an **Envoy xDS** construct. You produce a mapping row: the +feature, the xDS construct it lands on, the **ladder rung** it settled at, and +**why the higher (more native) rungs did not work**. + +You do not decide feasibility alone and you do not cite Envoy docs yourself. +You orchestrate two others: +- **`gateway-expert`** (skill, same session) — supplies current behavior, cited + to `gateway/**`. +- **`envoy-expert`** (subagent, Task tool) — supplies whether Envoy covers it, + cited to Envoy docs. + +## The ladder (the whole point) + +For every feature, climb from the top and **stop at the first rung that +holds**. Higher = more native, less custom code, less to maintain. + +``` +1. Envoy default / built-in → no filter config, native behavior covers it +2. Standard HTTP filter → jwt_authn, rate_limit, ext_authz, rbac, cors, ... +3. ext_proc / Lua / Wasm → custom logic in the proxy, no separate sidecar +4. new-Jumper sidecar → LAST RESORT: behavior has no in-proxy analog +``` + +**Bias, non-negotiable:** prefer Envoy defaults over custom logic in the +sidecar. Ask `envoy-expert` **default-first** — start the question at rung 1, +only descend when it answers NOT SUPPORTED. new-Jumper (rung 4) is reached +**only** when rungs 1-3 are exhausted, and the mapping row must justify it. + +The "new-Jumper" is the future Envoy-based replacement for the Jumper sidecar; +even for it, prefer Envoy defaults over reimplementing custom Jumper logic. + +## Procedure (one feature) + +1. **Current logic** — ask `gateway-expert`: what does this feature do today? + Get the feature file, the `IsUsed`/`Apply` behavior, and what it produces + (Kong plugin or `JumperConfig`/`RoutingConfigs` field), all cited. + Reduce it to a **behavioral requirement** — what must be true for a + request, independent of Kong. (E.g. AccessControl = "reject tokens whose + issuer isn't trusted AND whose consumer isn't in the allow-list.") + +2. **Climb the ladder** — take the behavioral requirement to `envoy-expert`, + phrased default-first: + > "Does Envoy cover natively? If not, is there a standard HTTP + > filter? If not, ext_proc/Lua/Wasm?" + Take its cited answer (SUPPORTED / PARTIAL / NOT SUPPORTED + mechanism). + Descend only as far as needed. + +3. **Emit the mapping row:** + ``` + | Feature | Current (cited) | xDS construct | Rung | Why not higher | Citation | + ``` + - PARTIAL → note the caveat and what closes the gap. + - Rung 4 (new-Jumper) → the "Why not higher" cell must show rungs 1-3 were + each ruled out (with the envoy-expert citation for each NOT SUPPORTED). + +## Feature reference (from gateway-expert) + +The 14 features and what they produce today live in the `gateway-expert` skill. +Ask it rather than re-deriving. The known-hard ones — LastMileSecurity, +ExternalIDP, CustomScopes, Claims, LoadBalancing, Failover — go through +`JumperConfig`/`RoutingConfigs` (the sidecar), so they are the most likely to +descend the ladder. Map the **easy independent** features first (AccessControl, +RateLimit, IpRestriction, HeaderTransformation) to validate the approach. + +## Rules + +- **One feature per pass.** Batch only by fanning out independent features in + parallel Task calls — never merge two features into one row. +- **No self-sourced Envoy claims.** Every "Envoy can/can't" comes from + `envoy-expert` with a citation. No citation → "unverified", descend or stop. +- **No self-sourced operator claims.** Every "today it does X" comes from + `gateway-expert` with a `path:line` citation. +- **Justify every descent.** A lower rung without a recorded reason the higher + rung failed is an incomplete row. +- You produce the **mapping**, not verdicts against the spec — that's + `evaluate-envoy`. Hand your table off to it for the requirements gate. + +## Output + +The mapping table (one row per feature) and nothing else, unless code is +explicitly requested. End with a one-line rung tally +(e.g. "3 default, 4 filter, 2 ext_proc, 5 new-Jumper"). diff --git a/.opencode/skills/map-logic/lms-token-issuing.md b/.opencode/skills/map-logic/lms-token-issuing.md new file mode 100644 index 000000000..6a8179dc0 --- /dev/null +++ b/.opencode/skills/map-logic/lms-token-issuing.md @@ -0,0 +1,88 @@ + + +# LastMileSecurity Token Issuing — Kong/Jumper → Envoy xDS + +Per-request flow: mint a new LastMileSecurity (LMS) JWT via an external issuer +service, inject it, then forward the original request to the real upstream API. + +## Mapping row + +| Feature | Current (cited) | xDS construct | Rung | Why not higher | +|---|---|---|---|---| +| LMS token issuing | Upstream repointed to local Jumper sidecar (`jumper.go:14`, `last_mile_security.go:57`); Jumper mints JWT from `JumperConfig.OAuth` (`jumper.go:55`) then calls real upstream. Req AU-03: new gateway-signed JWT from incoming claims + request context (`requirements.md:60`). | `ext_authz` HTTP filter → external issuer (new-Jumper) returns token via `OkHttpResponse.headers`; Envoy injects `Authorization`, routes to real upstream cluster | 3 (ext_authz) + 4 (issuer signs) | R1 no built-in issuer; R2 no stock filter signs; R3 Lua/Wasm have no private-key signing → signing descends to external issuer. Plumbing holds at rung 3. | + +## Today (Jumper as upstream) + +```mermaid +sequenceDiagram + participant C as Consumer + participant K as Kong (gateway) + participant J as Jumper sidecar
(localhost:8080/proxy) + participant U as Upstream API + + C->>K: request + consumer-token + K->>J: forward (upstream repointed to Jumper) + Note over J: mint + RS256/ES256-sign
new LMS JWT from
consumer-token claims + context + J->>U: request + Authorization: Bearer + U-->>J: response + J-->>K: response + K-->>C: response +``` + +## Target (Envoy ext_authz issuer call-out) + +```mermaid +sequenceDiagram + participant C as Consumer + participant E as Envoy + participant I as Issuer service
(new-Jumper, ext_authz) + participant U as Upstream API + + C->>E: request + consumer-token + E->>I: ext_authz Check (headers, opt. body) + Note over I: mint + RS256/ES256-sign
new LMS JWT
(holds private key) + I-->>E: OK + OkHttpResponse.headers
(Authorization: Bearer ) + Note over E: inject header, continue route + E->>U: original request + injected Authorization + U-->>E: response + E-->>C: response +``` + +Key difference: Envoy owns upstream routing; the issuer is a thin side call +that only mints and returns the token, it does not proxy. + +## Rung tally + +1 ext_authz filter (plumbing) + 1 new-Jumper issuer (signing only). + +## Setup plan + +Decisions (from code): +- Signing key scope: **per-realm** (issuer selects by route `RealmName`, `security_types.go:44`). +- Mechanism: **ext_authz** (issuer only returns a token; ext_proc streaming unneeded). +- Request body: **not needed** (`with_request_body` off) — all claim sources are headers/CRD spec. + +### P0 — Issuer service (new-Jumper) — blocks everything +1. **Key management** — per-realm signing keys in Secret/secret-manager; rotation; publish JWKS per realm for upstream verification. +2. **gRPC ext_authz Check server** — implement `envoy.service.auth.v3.Authorization`; mint + RS256/ES256-sign JWT; return `OkHttpResponse.headers` with `Authorization: Bearer `. +3. **Claim derivation** (AU-03): + - `realm`, `environment` ← route context headers (`last_mile_security.go:63`) + - `remote_api_url`, `api_base_path` ← upstream (`last_mile_security.go:87-88`) + - `aud` ← `Claim.Value` literal or `ValueFrom: ConsumerClientId` (`security_types.go:65-78`) + - `scope` ← `M2M.Scopes` (`security_types.go:149`) + - `sub`, `clientId`, `azp` ← incoming consumer-token + - `iss`, `exp`, `iat` ← issuer-computed per request + +### P1 — Envoy xDS wiring (operator emission) — depends on P0 +4. **ext_authz HTTP filter** in HCM chain → issuer cluster; `authorization_response.allowed_upstream_headers` = `Authorization`. +5. **Issuer cluster** — static/EDS cluster to the issuer service. +6. **FeatureBuilder Envoy path** — LMS branch: **stop repointing upstream to localhost** (`last_mile_security.go:57`); emit ext_authz + keep real upstream cluster. + +### P2 — Correctness & rollout +7. **Per-route enable** — LMS gated by `IsUsed` (`!PassThrough && no Failover`, `last_mile_security.go:43`); apply via `typed_per_filter_config`, not global. +8. **Failure mode** — `failure_mode_allow: false` (fail closed: issuer down → reject). +9. **Tests** — issuer unit (claim mapping, signing) + envtest for emitted xDS; upstream verifies via JWKS. diff --git a/gateway/api/v1/gateway_types.go b/gateway/api/v1/gateway_types.go index 6e06c38c6..772c9282e 100644 --- a/gateway/api/v1/gateway_types.go +++ b/gateway/api/v1/gateway_types.go @@ -24,8 +24,18 @@ type AdminConfig struct { Url string `json:"url"` } +const ( + GatewayClassNameKong = "kong" + GatewayClassNameEnvoy = "envoy" +) + // GatewaySpec defines the desired state of Gateway type GatewaySpec struct { + // gatewayClassName is the name of the GatewayClass that this Gateway belongs to. + // If this field is not specified, the default GatewayClass "kong" will be used. + // +optional + GatewayClassName string `json:"gatewayClassName,omitempty"` + Redis *RedisConfig `json:"redis,omitempty"` Admin AdminConfig `json:"admin"` diff --git a/gateway/cmd/main.go b/gateway/cmd/main.go index 9a8f3d17e..cfb4ae192 100644 --- a/gateway/cmd/main.go +++ b/gateway/cmd/main.go @@ -26,6 +26,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/controller" + "github.com/telekom/controlplane/gateway/internal/features/envoy" secretmetrics "github.com/telekom/controlplane/secret-manager/api/metrics" // +kubebuilder:scaffold:imports ) @@ -48,10 +49,12 @@ func main() { var probeAddr string var secureMetrics bool var enableHTTP2 bool + var xdsAddr string var tlsOpts []func(*tls.Config) flag.StringVar(&metricsAddr, "metrics-bind-address", "0", "The address the metrics endpoint binds to. "+ "Use :8443 for HTTPS or :8080 for HTTP, or leave as 0 to disable the metrics service.") flag.StringVar(&probeAddr, "health-probe-bind-address", "0", "The address the probe endpoint binds to.") + flag.StringVar(&xdsAddr, "xds-bind-address", ":18000", "The address the Envoy xDS (ADS) gRPC server binds to.") flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. "+ "Enabling this will ensure there is only one active controller manager.") @@ -139,6 +142,10 @@ func main() { rootCtx := ctrl.SetupSignalHandler() controller.RegisterIndecesOrDie(rootCtx, mgr) + // The shared, long-lived xDS snapshot cache. Reconcilers publish into it (on + // the leader) and the ADS server serves from it (on every replica). + xdsCache := envoy.NewXdsCache(ctrl.Log.WithName("envoy")) + if err = (&controller.GatewayReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), @@ -147,8 +154,9 @@ func main() { os.Exit(1) } if err = (&controller.RouteReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + XdsClient: xdsCache, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Route") os.Exit(1) @@ -169,6 +177,13 @@ func main() { } // +kubebuilder:scaffold:builder + // The Envoy xDS server runs on ALL replicas (not leader-gated) so each can + // serve its own connected Envoy pods. See internal/features/envoy/README.md. + if err := mgr.Add(envoy.NewServer(xdsCache.Cache(), xdsAddr)); err != nil { + setupLog.Error(err, "unable to add Envoy xDS server runnable") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { setupLog.Error(err, "unable to set up health check") os.Exit(1) diff --git a/gateway/config/crd/bases/gateway.cp.ei.telekom.de_gateways.yaml b/gateway/config/crd/bases/gateway.cp.ei.telekom.de_gateways.yaml index 08f21b308..35c6e99f5 100644 --- a/gateway/config/crd/bases/gateway.cp.ei.telekom.de_gateways.yaml +++ b/gateway/config/crd/bases/gateway.cp.ei.telekom.de_gateways.yaml @@ -62,6 +62,11 @@ spec: items: type: string type: array + gatewayClassName: + description: |- + gatewayClassName is the name of the GatewayClass that this Gateway belongs to. + If this field is not specified, the default GatewayClass "kong" will be used. + type: string redis: properties: enableTLS: diff --git a/gateway/go.mod b/gateway/go.mod index 9f4fe773c..1d0662cb1 100644 --- a/gateway/go.mod +++ b/gateway/go.mod @@ -26,6 +26,8 @@ tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen require ( github.com/emirpasic/gods v1.18.1 + github.com/envoyproxy/go-control-plane v0.14.0 + github.com/envoyproxy/go-control-plane/envoy v1.37.0 github.com/go-logr/logr v1.4.3 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.5.0 @@ -34,6 +36,8 @@ require ( github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af k8s.io/api v0.36.2 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 @@ -50,9 +54,12 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chigopher/pathlib v0.19.1 // indirect + github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -90,6 +97,7 @@ require ( github.com/oasdiff/yaml v0.1.1 // indirect github.com/oasdiff/yaml3 v0.0.14 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect @@ -136,8 +144,6 @@ require ( gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect - google.golang.org/grpc v1.80.0 // indirect - google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/gateway/go.sum b/gateway/go.sum index bf6fac9b3..69e248cd4 100644 --- a/gateway/go.sum +++ b/gateway/go.sum @@ -21,6 +21,8 @@ github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= +github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -35,6 +37,14 @@ github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bF github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= +github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= +github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= +github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4= +github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= @@ -196,6 +206,8 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/gateway/internal/controller/route_controller.go b/gateway/internal/controller/route_controller.go index 72b8c0dbe..bffd33a39 100644 --- a/gateway/internal/controller/route_controller.go +++ b/gateway/internal/controller/route_controller.go @@ -20,6 +20,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + "github.com/telekom/controlplane/gateway/internal/features/envoy" routehandler "github.com/telekom/controlplane/gateway/internal/handler/route" ) @@ -29,6 +30,10 @@ type RouteReconciler struct { Scheme *runtime.Scheme Recorder record.EventRecorder + // XdsClient is the shared Envoy xDS client, injected at startup and passed to + // the RouteHandler for Envoy-class Gateways. May be nil in Kong-only setups. + XdsClient envoy.XdsClient + cc.Controller[*gatewayv1.Route] } @@ -44,7 +49,7 @@ func (r *RouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl // SetupWithManager sets up the controller with the Manager. func (r *RouteReconciler) SetupWithManager(mgr ctrl.Manager) error { r.Recorder = mgr.GetEventRecorderFor("route-controller") - r.Controller = cc.NewController(&routehandler.RouteHandler{}, r.Client, r.Recorder) + r.Controller = cc.NewController(&routehandler.RouteHandler{XdsClient: r.XdsClient}, r.Client, r.Recorder) return ctrl.NewControllerManagedBy(mgr). For(&gatewayv1.Route{}). diff --git a/gateway/internal/features/envoy/README.md b/gateway/internal/features/envoy/README.md new file mode 100644 index 000000000..10f6121d9 --- /dev/null +++ b/gateway/internal/features/envoy/README.md @@ -0,0 +1,207 @@ + + +# Envoy Feature Builder + +Tracks how the Envoy `FeatureBuilder` replaces the Kong-based builder. Each Kong +feature emits Kong plugin / Jumper config today; the Envoy builder must reproduce +the same behaviour via Envoy xDS. + +**Current state:** `builder.go` is a stub (`Build`/`BuildForConsumer` return +`BlockedErrorf("... not implemented yet")`) and `feature/access_control.go` +panics. Every feature below is **Planned**. + +## Feature Support Matrix + +Legend: **Done** = implemented & wired · **WIP** = in progress · **Planned** = not started. + +### Independent Features + +| Feature | Kong Priority | Purpose | Envoy | +|---|---|---|---| +| PassThrough | 0 | Route straight to upstream(s), no last-mile security (`route.Spec.PassThrough`). | Planned | +| AccessControl | 10 | ACL: restrict access by the route's trusted issuers. | Planned | +| RateLimit | 10 | Limit request rate per route/consumer (Redis-backed). | Planned | +| HeaderTransformation | 0 | Add/modify request & response headers on the primary route. | Planned | +| BasicAuth | 10 | HTTP Basic auth for consumers. | Planned | +| IpRestriction | 10 | Allow/deny IPs or CIDRs, per consumer. | Planned | +| CircuitBreaker | 110 | Trip traffic away from failing upstreams; rewrites service host (highest priority). | Planned | +| DynamicUpstream | LMS+1 (101) | Set `remote_api_url` dynamically; overrides upstream resolution. | Planned | + +### Dependent Features + +| Feature | Kong Priority | Depends On | Purpose | Envoy | +|---|---|---|---|---| +| LastMileSecurity | 100 | AccessControl | Inject/validate last-mile JWT (Jumper) gateway↔upstream. | Planned | +| ExternalIDP | CustomScopes-1 (98) | LastMileSecurity | Exchange tokens against an external IDP. | Planned | +| CustomScopes | LMS-1 (99) | LastMileSecurity | Attach custom OAuth scopes to the outgoing token. | Planned | +| Claims | 10 | LastMileSecurity | Write provider exposure token claims into JumperConfig. | Planned | +| LoadBalancing | LMS+2 (102) | LastMileSecurity | Distribute across multiple upstreams (>1). | Planned | +| Failover | CircuitBreaker-1 (109) | LastMileSecurity | Route to a secondary upstream when the primary is down. | Planned | + +### Constraints + +- **ExternalIDP and LastMileSecurity are mutually exclusive** in the new builder + (differs from Kong, where ExternalIDP depends on LastMileSecurity). +- Today Kong's LastMileSecurity forwards to the Jumper sidecar, which owns all + other token/upstream features via `remote_api_url`. Jumper will be **removed** + or **split into narrow-scope components** — not one monolithic sidecar. +- Kong features implement `features.KongFeature`; Envoy features implement + `features.EnvoyFeature` (`internal/features/interfaces.go`). +- Priorities determine apply order; documented so the Envoy builder can preserve + equivalent ordering. + +## Domain ↔ xDS Vocabulary + +`A == B` means "our A is Envoy's B". xDS types are v3 API types. + +| Domain concept | Envoy xDS resource / field | xDS API type | +|---|---|---| +| Gateway | The xDS node fed via ADS (LDS/RDS/CDS/EDS). Not a config object. | `config.bootstrap.v3.Bootstrap` (node) | +| Route (CRD) | A `VirtualHost` with N `Route` entries — **not** a single Envoy `Route`. | `route.v3.VirtualHost` + `route.v3.Route` | +| Route.Hostnames | `VirtualHost.domains` (empty = `["*"]`). | `route.v3.VirtualHost.domains` | +| Route.Paths | `Route.match`, one Envoy `Route` per path (empty = `prefix: "/"`). | `route.v3.RouteMatch` | +| Backend.Upstreams (>1) | `RouteAction.weighted_clusters` → one `ClusterWeight` per upstream. | `route.v3.WeightedCluster.ClusterWeight` | +| Backend.Upstreams (single) | `RouteAction.cluster`. | `route.v3.RouteAction.cluster` | +| Upstream (single target) | An `LbEndpoint` in the Cluster's `ClusterLoadAssignment`. | `endpoint.v3.ClusterLoadAssignment` / `LbEndpoint` | +| Upstream weight | `ClusterWeight.weight` or `LbEndpoint.load_balancing_weight`. | as above | +| Route.Type primary/secondary | Endpoint priority (P0/P1) in one CLA, or an aggregate cluster. | `LocalityLbEndpoints.priority`; `envoy.clusters.aggregate` | +| Route.Type proxy | A `Cluster` whose endpoints are other Envoys (mesh hop). No dedicated type. | `cluster.v3.Cluster` | +| PassThrough | Disable auth filters via `typed_per_filter_config`. | per-route `typed_per_filter_config` | +| Security.TrustedIssuers | One `JwtProvider` per issuer, referenced by `requires`. | `jwt_authn` — `JwtProvider.issuer` | +| Consumer | Authenticated principal (JWT metadata / RBAC). No resource. | `jwt_authn` + `rbac` | +| ConsumeRoute | Per-route RBAC policy matching the consumer's principal. | `rbac` — `Policy` / `Principal` | +| ConsumeRoute rate limit | Rate-limit descriptor keyed on consumer identity; external RLS. | `ratelimit` — `route.v3.RateLimit` | +| Consumer IP restriction | RBAC `remote_ip` / `direct_remote_ip` CIDR. | `rbac` — `Principal.remote_ip` | +| Transformation | Route/vhost header add/remove, or Header Mutation filter. | `route.v3.Route` header fields; `header_mutation` | +| Buffering | Buffer filter (request); route `request_body_buffer_limit`. Response buffering not first-class. | `buffer` | + +**Not 1:1 — watch out:** + +- **"Route" ≠ Envoy `Route`** — ours = a `VirtualHost` with N `Route` entries. Three xDS levels. +- **primary/secondary** — endpoint priority (P0/P1, recommended, health-driven) vs. aggregate cluster (when cluster-level config differs). Decision open. +- **`proxy`** — no Envoy equivalent; just a `Cluster` pointing at another Envoy. +- **Consumer** — no registry; create/delete mutates JWT providers + RBAC principals on affected routes. Biggest gap. +- **ConsumeRoute** fragments across RBAC + ratelimit descriptor + per-filter M2M config. No single xDS object. +- **Response-body buffering** — not a first-class toggle; likely ext_proc/Lua. + +## Builder Structure + +The `EnvoyFeatureBuilder` reuses Kong's **execution model** (the `Feature` +contract `Name/Priority/IsUsed/Apply` + sorted, sequential, fail-fast apply loop) +but not its state shape. + +**Keep from Kong:** +- Enable → ordered apply → assemble three-phase flow. +- `SortFeatures` by `Priority()` ascending, then `if IsUsed { Apply }`, sequential, fail-fast (requeue on first error). +- Shared feature-mutated state in the builder so later features see earlier writes. + +**Drop / change:** +- **No `map[string]cachedProto` with panic getters** — use typed draft state; wrong types = compile errors, invariant violations = `error`, never panic. +- **No per-plugin create/replace/cleanup** — Envoy pushes one bundle. +- **Narrow mutation surface** — small helpers (`EnsureVirtualHost`, `EnsureCluster`, `AddJWTProvider`, `AddRBACPolicy`, `AddRateLimitDescriptor`), not raw proto access. +- **Deterministic ordering** — sort map keys before emitting slices. + +**Priority vs. filter-chain order:** `Priority()` orders feature computation / +data dependencies only. Envoy's HTTP filter order (jwt_authn → rbac → ratelimit) +is protocol-semantic and fixed by a **canonical order table in the assemble +phase**, never derived from priority. + +## Production Architecture + +The unit of correctness is the **node snapshot** (all resources consistent +together), not a per-route push. `XdsClient.SetSnapshotFor(ctx, nodeID, bundle)` +replaces the whole node bundle. + +**Topology (scale-defining):** multiple CP-Gateways, each pinned to a region; +each == one Envoy Deployment scaled to **100+ identical pods** (HPA). One control +plane per region serves all pods via xDS/ADS. + +**One snapshot per Gateway, keyed by Gateway identity:** +- go-control-plane's `SnapshotCache` keys on `NodeHash.ID(node)`, not the stream. +- **Do NOT use default `IDHash`** (per-pod `node.id`) → 100+ duplicate snapshots. +- **Use a custom `NodeHash`** keying on `node.metadata.role` (e.g. `region~gateway`) → one snapshot, one `SetSnapshot` fans out to all pods. +- Recompute is **O(1) in pod count**; only fan-out/serving is O(pods). +- Zone-aware LB needs no per-pod EDS — Envoy runs a locality heuristic against the shared CLA using each pod's `node.locality.zone` (set via Downward API). +- **Per-connected-client (kgateway UCC) not needed** — replicas are identical. + +**Model (full-recompute-per-Gateway + hash-diff gate + shared node key)**, taken +from [kgateway](https://github.com/kgateway-dev/kgateway) — see [Designing +kgateway for scalability](https://kgateway.dev/blog/design-kgateway-for-scalability/): +- **NodeSnapshotBuilder** (per Gateway) recomputes all routes into one `ResourceBundle` on any change; runs each route's feature loop; canonicalizes filter order; validates referential integrity before publish. +- **Hash-diff gate** — push only if the content hash changed. Mandatory at 100+ streams (else all pods re-ACK). +- **Single writer per Gateway key**; **retain last-good** on incomplete input; **content-hash versions** for restart determinism. +- The per-Route `EnvoyFeatureBuilder` runs the feature loop but **does not push**; the node builder aggregates and owns `SetSnapshotFor`. +- `BuildForConsumer` is a **conceptual mismatch** (Consumer = RBAC principal / JWT identity) — keep only as a shim that marks routes stale and triggers recompute. + +**CP HA & scale:** +- One CP deployment per region; each replica has its own in-memory `SnapshotCache`. No cross-replica coordination. +- xDS serving is **not** leader-gated; only K8s writes are (verified vs. istiod + kgateway). +- Multiple stateless replicas independently recompute the same deterministic snapshot. **No shared snapshot store, no sticky sessions** for correctness. +- **Readiness gating (required):** a fresh replica parks watches until its first `SetSnapshot`; gate readiness on "first snapshot for all served Gateways". Raise Envoy `init_fetch_timeout` (default 15s) if warm-up can exceed it. + +**Two-tier leader election (controller-runtime):** we already have leader +election (`cmd/main.go:120`, `--leader-elect`) gating reconcilers. The xDS work +must run on **all** replicas — add a runnable with `NeedLeaderElection() == false`. + +| Concern | Runs on | Why safe | +|---|---|---| +| Reconcile → status writes, finalizers, child CRs | Leader only | Single writer → no API-server races | +| Watch CRDs → build snapshot → serve xDS | All replicas | Read-only + idempotent; serves own connections | + +The snapshot builder never writes to Kubernetes — it reads CRDs via the manager's +shared informer cache and serves an in-memory projection. It must be driven by +**informer events on the manager cache**, not the reconcile loop (which runs on +the leader only) — kgateway uses a collection layer +([krt](https://github.com/istio/istio/blob/master/pkg/kube/krt/README.md)) for this. + +**Fan-out hazards at 100+ streams:** +- **ADS serial fan-out under lock** — every version bump is O(100) serial; budget & monitor. +- **Blocking response channels** — a stuck pod can back-pressure fan-out. +- **Thundering-herd re-ACK** — the concrete reason the hash-diff gate is non-negotiable. + +**Operational rule:** never `ClearSnapshot` on pod disconnect (snapshot is +shared) — clear only when the whole Gateway is deleted. + +> [!NOTE] +> Refuted: there is no `PILOT_ENABLE_XDS_LOAD_BALANCING` / connection-rebalancing +> knob in current Istio. Plan for connection stickiness to a replica for the +> stream's lifetime. + +## Decision Checklist + +**Decided (verified — implement as stated):** + +1. **Snapshot granularity** — one per Gateway, shared by all pods; full recompute per Gateway on change. +2. **`NodeHash`** — key on `node.metadata.role` (e.g. `region~gateway`); never default `IDHash`. +3. **Versioning** — content hash of the assembled bundle; push only on change. +4. **CP HA / leader election** — reuse existing election for reconcilers; add xDS + snapshot builder as non-leader-gated runnable. No shared store, no sticky sessions. +5. **Trigger** — informer/`EventHandler` on the manager cache, not the reconcile loop. +6. **Failure policy** — retain last-good; never push partial. +7. **Readiness gating** — gate on "first snapshot for all served Gateways". +8. **Consumer path** — no `BuildForConsumer` pipeline; changes mark routes stale + recompute. +9. **Pod `node.locality.zone`** — wire via Downward API for zone-aware LB from the shared CLA. +10. **Operational** — never `ClearSnapshot` on pod disconnect; only on Gateway deletion. + +**Open (needs a call):** + +11. **Failover encoding** — endpoint priority (P0/P1) vs. aggregate cluster. +12. **Merge/recompute contract** — route enumeration, dedupe keys, conflict resolution. +13. **Canonical filter-order table** — fixed jwt_authn → rbac → ratelimit → … in assemble. +14. **Event coalescing** — debounce CRD bursts; single-writer serialization per Gateway. +15. **Consistency validation** — RDS↔CDS↔EDS referential integrity before publish. +16. **`init_fetch_timeout` vs. warm-up** — measure CP cold-start, tune if needed. +17. **Observability** — snapshot version, push count, suppressed pushes, validation failures, last-good age, stream count. +18. **Response-body buffering** — confirm feasibility (likely ext_proc/Lua). + +## Per-Feature xDS Mapping + +For each feature: what Kong/Jumper does today, the chosen Envoy xDS mapping, and +why. (To be filled in via `map-logic`.) + +_PassThrough · AccessControl · HeaderTransformation · BasicAuth · IpRestriction · +CircuitBreaker · DynamicUpstream · RateLimit · LastMileSecurity · ExternalIDP · +CustomScopes · Claims · LoadBalancing · Failover — all TBD._ diff --git a/gateway/internal/features/envoy/access_control_test.go b/gateway/internal/features/envoy/access_control_test.go new file mode 100644 index 000000000..7aba9a8f4 --- /dev/null +++ b/gateway/internal/features/envoy/access_control_test.go @@ -0,0 +1,181 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy_test + +import ( + "context" + + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + rbacconfigv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" + rbacfilterv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/telekom/controlplane/common/pkg/types" + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + "github.com/telekom/controlplane/gateway/internal/features/envoy" + "github.com/telekom/controlplane/gateway/internal/features/envoy/feature" +) + +var _ = Describe("AccessControl feature", func() { + var ( + ctx context.Context + xds *envoy.XdsCache + cache cachev3.SnapshotCache + ) + + const upstreamHTTP = "http" + + BeforeEach(func() { + ctx = context.Background() + xds = envoy.NewXdsCache(GinkgoLogr) + var ok bool + cache, ok = xds.Cache().(cachev3.SnapshotCache) + Expect(ok).To(BeTrue()) + }) + + buildWith := func(route *gatewayv1.Route, consumers ...*gatewayv1.ConsumeRoute) *hcmv3.HttpConnectionManager { + b := envoy.NewEnvoyFeatureBuilder(xds, route, nil, &gatewayv1.Gateway{}) + b.AddAllowedConsumers(consumers...) + b.EnableFeature(feature.InstanceAccessControlFeature) + Expect(b.Build(ctx)).To(Succeed()) + + snap, err := cache.GetSnapshot(route.Spec.GatewayRef.Name) + Expect(err).NotTo(HaveOccurred()) + for _, r := range snap.GetResources(resourcev3.ListenerType) { + l := r.(*listenerv3.Listener) + hcm := &hcmv3.HttpConnectionManager{} + Expect(l.GetFilterChains()[0].GetFilters()[0].GetTypedConfig().UnmarshalTo(hcm)).To(Succeed()) + return hcm + } + Fail("no listener in snapshot") + return nil + } + + filterNames := func(hcm *hcmv3.HttpConnectionManager) []string { + var names []string + for _, f := range hcm.GetHttpFilters() { + names = append(names, f.GetName()) + } + return names + } + + unmarshalRBAC := func(hcm *hcmv3.HttpConnectionManager) *rbacconfigv3.RBAC { + for _, f := range hcm.GetHttpFilters() { + if f.GetName() == "envoy.filters.http.rbac" { + wrapper := &rbacfilterv3.RBAC{} + Expect(f.GetTypedConfig().UnmarshalTo(wrapper)).To(Succeed()) + return wrapper.GetRules() + } + } + Fail("no rbac filter") + return nil + } + + unmarshalJWT := func(hcm *hcmv3.HttpConnectionManager) *jwtauthnv3.JwtAuthentication { + for _, f := range hcm.GetHttpFilters() { + if f.GetName() == "envoy.filters.http.jwt_authn" { + cfg := &jwtauthnv3.JwtAuthentication{} + Expect(f.GetTypedConfig().UnmarshalTo(cfg)).To(Succeed()) + return cfg + } + } + Fail("no jwt_authn filter") + return nil + } + + acRoute := func(issuers, defaultConsumers []string, disable bool) *gatewayv1.Route { + return &gatewayv1.Route{ + ObjectMeta: metav1.ObjectMeta{Name: "my-route", Namespace: "ns"}, + Spec: gatewayv1.RouteSpec{ + GatewayRef: types.ObjectRef{Name: "my-gw"}, + Paths: []string{"/"}, + Backend: gatewayv1.Backend{Upstreams: []gatewayv1.Upstream{{Scheme: upstreamHTTP, Hostname: "backend", Port: 8080}}}, + Security: gatewayv1.Security{ + TrustedIssuers: issuers, + DefaultConsumers: defaultConsumers, + DisableAccessControl: disable, + RealmName: "realm", + }, + }, + } + } + + consumeRoute := func(consumerName string) *gatewayv1.ConsumeRoute { + return &gatewayv1.ConsumeRoute{ + Spec: gatewayv1.ConsumeRouteSpec{ + Route: types.ObjectRef{Name: "my-route", Namespace: "ns"}, + ConsumerName: consumerName, + }, + } + } + + It("does not add auth filters when the route has no trusted issuers", func() { + hcm := buildWith(acRoute(nil, nil, false)) + Expect(filterNames(hcm)).To(Equal([]string{"envoy.filters.http.router"})) + }) + + It("adds jwt_authn and rbac before the router, in order", func() { + hcm := buildWith(acRoute([]string{"https://kc/realms/a"}, []string{"c1"}, false)) + Expect(filterNames(hcm)).To(Equal([]string{ + "envoy.filters.http.jwt_authn", + "envoy.filters.http.rbac", + "envoy.filters.http.router", + })) + }) + + It("creates one jwt provider per trusted issuer, all requires_any", func() { + hcm := buildWith(acRoute([]string{"https://kc/realms/a", "https://kc/realms/b"}, []string{"c1"}, false)) + jwt := unmarshalJWT(hcm) + Expect(jwt.GetProviders()).To(HaveLen(2)) + reqs := jwt.GetRules()[0].GetRequires().GetRequiresAny().GetRequirements() + Expect(reqs).To(HaveLen(2)) + }) + + It("allow-lists default consumers plus route-matched allowed consumers", func() { + hcm := buildWith( + acRoute([]string{"https://kc/realms/a"}, []string{"default-c"}, false), + consumeRoute("allowed-c"), + // belongs to a different route -> excluded + &gatewayv1.ConsumeRoute{Spec: gatewayv1.ConsumeRouteSpec{ + Route: types.ObjectRef{Name: "other-route", Namespace: "ns"}, ConsumerName: "excluded-c", + }}, + ) + rbac := unmarshalRBAC(hcm) + Expect(rbac.GetAction()).To(Equal(rbacconfigv3.RBAC_ALLOW)) + principals := rbac.GetPolicies()["allow-consumers"].GetPrincipals() + + var matched []string + for _, p := range principals { + matched = append(matched, p.GetMetadata().GetValue().GetStringMatch().GetExact()) + } + Expect(matched).To(ConsistOf("default-c", "allowed-c")) + + // path is [payload key, azp] + seg := principals[0].GetMetadata().GetPath() + Expect(seg).To(HaveLen(2)) + Expect(seg[1].GetKey()).To(Equal("azp")) + }) + + It("empty allow-list yields an ALLOW rbac with no policies (deny all)", func() { + hcm := buildWith(acRoute([]string{"https://kc/realms/a"}, nil, false)) + rbac := unmarshalRBAC(hcm) + Expect(rbac.GetAction()).To(Equal(rbacconfigv3.RBAC_ALLOW)) + Expect(rbac.GetPolicies()).To(BeEmpty()) + }) + + It("skips rbac when access control is disabled but keeps jwt_authn", func() { + hcm := buildWith(acRoute([]string{"https://kc/realms/a"}, []string{"c1"}, true)) + Expect(filterNames(hcm)).To(Equal([]string{ + "envoy.filters.http.jwt_authn", + "envoy.filters.http.router", + })) + }) +}) diff --git a/gateway/internal/features/envoy/builder.go b/gateway/internal/features/envoy/builder.go new file mode 100644 index 000000000..bd1ed4aac --- /dev/null +++ b/gateway/internal/features/envoy/builder.go @@ -0,0 +1,149 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy + +import ( + "context" + + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + "github.com/go-logr/logr" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/telekom/controlplane/common/pkg/errors/ctrlerrors" + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + "github.com/telekom/controlplane/gateway/internal/features" +) + +var _ features.EnvoyFeatureBuilder = &Builder{} + +type Builder struct { + client XdsClient + + AllowedConsumers []*gatewayv1.ConsumeRoute + + Route *gatewayv1.Route + Consumer *gatewayv1.Consumer + Gateway *gatewayv1.Gateway + + Features map[gatewayv1.FeatureType]features.EnvoyFeature + + // httpFilters are the extra HTTP filters contributed by features during + // Apply, in application order. buildListener inserts them before the router. + httpFilters []*hcmv3.HttpFilter +} + +var NewEnvoyFeatureBuilder = func(xdsClient XdsClient, route *gatewayv1.Route, consumer *gatewayv1.Consumer, gateway *gatewayv1.Gateway) features.EnvoyFeatureBuilder { + return &Builder{ + client: xdsClient, + AllowedConsumers: []*gatewayv1.ConsumeRoute{}, + Route: route, + Consumer: consumer, + Gateway: gateway, + Features: map[gatewayv1.FeatureType]features.EnvoyFeature{}, + } +} + +// EnableFeature implements [features.EnvoyFeatureBuilder]. +func (b *Builder) EnableFeature(f features.EnvoyFeature) { + b.Features[f.Name()] = f +} + +// AddHTTPFilter implements [features.EnvoyFeatureBuilder]. +func (b *Builder) AddHTTPFilter(name string, typedConfig *anypb.Any) { + b.httpFilters = append(b.httpFilters, &hcmv3.HttpFilter{ + Name: name, + ConfigType: &hcmv3.HttpFilter_TypedConfig{TypedConfig: typedConfig}, + }) +} + +// GetRoute implements [features.FeatureBuilder]. +func (b *Builder) GetRoute() (*gatewayv1.Route, bool) { + if b.Route == nil { + return nil, false + } + return b.Route, true +} + +// GetConsumer implements [features.FeatureBuilder]. +func (b *Builder) GetConsumer() (*gatewayv1.Consumer, bool) { + if b.Consumer == nil { + return nil, false + } + return b.Consumer, true +} + +// GetGateway implements [features.FeatureBuilder]. +func (b *Builder) GetGateway() *gatewayv1.Gateway { + return b.Gateway +} + +// GetAllowedConsumers implements [features.FeatureBuilder]. +func (b *Builder) GetAllowedConsumers() []*gatewayv1.ConsumeRoute { + return b.AllowedConsumers +} + +// AddAllowedConsumers implements [features.FeatureBuilder]. +func (b *Builder) AddAllowedConsumers(consumers ...*gatewayv1.ConsumeRoute) { + b.AllowedConsumers = append(b.AllowedConsumers, consumers...) +} + +// Build implements [features.FeatureBuilder]. It applies the enabled features in +// priority order (each may contribute HTTP filters via AddHTTPFilter), then +// renders the core-routing xDS resources for the Route and publishes them as one +// consistent node snapshot. +func (b *Builder) Build(ctx context.Context) error { + log := logr.FromContextOrDiscard(ctx).WithName("envoy.features.builder") + if b.Route == nil { + return features.ErrNoRoute + } + log = log.WithValues("route", b.Route.Name) + + for _, f := range features.SortFeatures(features.ToSlice(b.Features)) { + if f.IsUsed(ctx, b) { + log.V(1).Info("Applying feature", "name", f.Name()) + if err := f.Apply(ctx, b); err != nil { + return err + } + } else { + log.V(1).Info("Feature is not used", "name", f.Name()) + } + } + + upstreams := b.Route.Spec.Backend.Upstreams + if len(upstreams) == 0 { + return ctrlerrors.BlockedErrorf("route %q has no upstream", b.Route.Name) + } + // ponytail: single-upstream only; weighted_clusters for len>1 is a later + // increment (see RT-04/RT-11). Take the first target. + upstream := upstreams[0] + + bundle, err := renderCoreRouting(b.Route, upstream, b.httpFilters) + if err != nil { + return ctrlerrors.RetryableErrorf("rendering xDS for route %q: %v", b.Route.Name, err) + } + + nodeID := nodeIDForRoute(b.Route) + log.V(0).Info("Publishing route snapshot", "nodeID", nodeID) + if err := b.client.SetSnapshotFor(ctx, nodeID, bundle); err != nil { + return ctrlerrors.RetryableErrorf("publishing snapshot for route %q: %v", b.Route.Name, err) + } + return nil +} + +// BuildForConsumer implements [features.FeatureBuilder]. +// ponytail: consumer-scoped features (e.g. IpRestriction) are a later increment. +func (b *Builder) BuildForConsumer(context.Context) error { + return ctrlerrors.BlockedErrorf("Envoy BuildForConsumer is not implemented yet") +} + +// nodeIDForRoute keys the snapshot on the Gateway the Route targets, matching the +// nodeHash convention (node.metadata.role == Gateway identity). +// +// ponytail: keyed per-Route on the Gateway name, but each Route currently +// overwrites the Gateway's whole snapshot (single-route-per-node). Accumulate all +// routes of a Gateway into one bundle before this is production-shaped. +func nodeIDForRoute(route *gatewayv1.Route) string { + return route.Spec.GatewayRef.Name +} diff --git a/gateway/internal/features/envoy/builder_test.go b/gateway/internal/features/envoy/builder_test.go new file mode 100644 index 000000000..9924159d6 --- /dev/null +++ b/gateway/internal/features/envoy/builder_test.go @@ -0,0 +1,175 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy_test + +import ( + "context" + + clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/telekom/controlplane/common/pkg/types" + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + "github.com/telekom/controlplane/gateway/internal/features" + "github.com/telekom/controlplane/gateway/internal/features/envoy" +) + +func makeRoute(name, gatewayName string, hostnames, paths []string, upstreams ...gatewayv1.Upstream) *gatewayv1.Route { + return &gatewayv1.Route{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: gatewayv1.RouteSpec{ + GatewayRef: types.ObjectRef{Name: gatewayName}, + Hostnames: hostnames, + Paths: paths, + Backend: gatewayv1.Backend{Upstreams: upstreams}, + }, + } +} + +var _ = Describe("Builder.Build core routing", func() { + var ( + ctx context.Context + xds *envoy.XdsCache + cache cachev3.SnapshotCache + ) + + BeforeEach(func() { + ctx = context.Background() + xds = envoy.NewXdsCache(GinkgoLogr) + var ok bool + cache, ok = xds.Cache().(cachev3.SnapshotCache) + Expect(ok).To(BeTrue()) + }) + + build := func(route *gatewayv1.Route) features.EnvoyFeatureBuilder { + b := envoy.NewEnvoyFeatureBuilder(xds, route, nil, &gatewayv1.Gateway{}) + Expect(b.Build(ctx)).To(Succeed()) + return b + } + + snapshotFor := func(gatewayName string) cachev3.ResourceSnapshot { + snap, err := cache.GetSnapshot(gatewayName) + Expect(err).NotTo(HaveOccurred()) + return snap + } + + It("errors when the route has no upstream", func() { + route := makeRoute("r", "gw", nil, nil) + b := envoy.NewEnvoyFeatureBuilder(xds, route, nil, &gatewayv1.Gateway{}) + Expect(b.Build(ctx)).NotTo(Succeed()) + }) + + It("publishes listener, routeconfig and cluster keyed on the gateway", func() { + route := makeRoute("my-route", "my-gw", + []string{"api.example.com"}, []string{"/api"}, + gatewayv1.Upstream{Scheme: "http", Hostname: "backend", Port: 8080}) + build(route) + + snap := snapshotFor("my-gw") + Expect(snap.GetResources(resourcev3.ListenerType)).To(HaveLen(1)) + Expect(snap.GetResources(resourcev3.RouteType)).To(HaveLen(1)) + Expect(snap.GetResources(resourcev3.ClusterType)).To(HaveLen(1)) + }) + + It("maps hostnames to vhost domains and paths to prefix routes", func() { + route := makeRoute("my-route", "my-gw", + []string{"api.example.com", "www.example.com"}, []string{"/v1", "/v2"}, + gatewayv1.Upstream{Scheme: "http", Hostname: "backend", Port: 8080}) + build(route) + + rc := unmarshalRouteConfig(snapshotFor("my-gw")) + Expect(rc.VirtualHosts).To(HaveLen(1)) + Expect(rc.VirtualHosts[0].Domains).To(ConsistOf("api.example.com", "www.example.com")) + + prefixes := []string{} + for _, r := range rc.VirtualHosts[0].Routes { + prefixes = append(prefixes, r.GetMatch().GetPrefix()) + Expect(r.GetRoute().GetCluster()).To(Equal("my-route-cluster")) + } + Expect(prefixes).To(ConsistOf("/v1", "/v2")) + }) + + It("defaults empty hostnames to * and empty paths to /", func() { + route := makeRoute("my-route", "my-gw", nil, nil, + gatewayv1.Upstream{Scheme: "http", Hostname: "backend", Port: 8080}) + build(route) + + rc := unmarshalRouteConfig(snapshotFor("my-gw")) + Expect(rc.VirtualHosts[0].Domains).To(ConsistOf("*")) + Expect(rc.VirtualHosts[0].Routes).To(HaveLen(1)) + Expect(rc.VirtualHosts[0].Routes[0].GetMatch().GetPrefix()).To(Equal("/")) + }) + + It("wires the HCM to RDS via ADS with the router filter", func() { + route := makeRoute("my-route", "my-gw", nil, []string{"/"}, + gatewayv1.Upstream{Scheme: "http", Hostname: "backend", Port: 8080}) + build(route) + + hcm := unmarshalHCM(snapshotFor("my-gw")) + Expect(hcm.GetRds().GetRouteConfigName()).To(Equal("my-route-routes")) + Expect(hcm.GetRds().GetConfigSource().GetAds()).NotTo(BeNil()) + Expect(hcm.GetHttpFilters()).To(HaveLen(1)) + Expect(hcm.GetHttpFilters()[0].GetName()).To(Equal("envoy.filters.http.router")) + }) + + It("builds a STRICT_DNS cluster to the upstream target without TLS for http", func() { + route := makeRoute("my-route", "my-gw", nil, []string{"/"}, + gatewayv1.Upstream{Scheme: "http", Hostname: "backend", Port: 8080}) + build(route) + + c := unmarshalCluster(snapshotFor("my-gw")) + Expect(c.GetType()).To(Equal(clusterv3.Cluster_STRICT_DNS)) + sa := c.GetLoadAssignment().GetEndpoints()[0].GetLbEndpoints()[0]. + GetEndpoint().GetAddress().GetSocketAddress() + Expect(sa.GetAddress()).To(Equal("backend")) + Expect(sa.GetPortValue()).To(Equal(uint32(8080))) + Expect(c.GetTransportSocket()).To(BeNil()) + }) + + It("adds a TLS transport socket for https upstreams", func() { + route := makeRoute("my-route", "my-gw", nil, []string{"/"}, + gatewayv1.Upstream{Scheme: "https", Hostname: "secure.backend", Port: 443}) + build(route) + + c := unmarshalCluster(snapshotFor("my-gw")) + Expect(c.GetTransportSocket()).NotTo(BeNil()) + Expect(c.GetTransportSocket().GetName()).To(Equal("envoy.transport_sockets.tls")) + }) +}) + +func unmarshalRouteConfig(snap cachev3.ResourceSnapshot) *routev3.RouteConfiguration { + for _, r := range snap.GetResources(resourcev3.RouteType) { + return r.(*routev3.RouteConfiguration) + } + Fail("no route configuration in snapshot") + return nil +} + +func unmarshalCluster(snap cachev3.ResourceSnapshot) *clusterv3.Cluster { + for _, r := range snap.GetResources(resourcev3.ClusterType) { + return r.(*clusterv3.Cluster) + } + Fail("no cluster in snapshot") + return nil +} + +func unmarshalHCM(snap cachev3.ResourceSnapshot) *hcmv3.HttpConnectionManager { + for _, r := range snap.GetResources(resourcev3.ListenerType) { + l := r.(*listenerv3.Listener) + filter := l.GetFilterChains()[0].GetFilters()[0] + hcm := &hcmv3.HttpConnectionManager{} + Expect(filter.GetTypedConfig().UnmarshalTo(hcm)).To(Succeed()) + return hcm + } + Fail("no listener in snapshot") + return nil +} diff --git a/gateway/internal/features/envoy/feature/access_control.go b/gateway/internal/features/envoy/feature/access_control.go new file mode 100644 index 000000000..78a18530a --- /dev/null +++ b/gateway/internal/features/envoy/feature/access_control.go @@ -0,0 +1,232 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package feature + +import ( + "context" + "fmt" + "time" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + rbacconfigv3 "github.com/envoyproxy/go-control-plane/envoy/config/rbac/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" + rbachttpv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/rbac/v3" + matcherv3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "github.com/go-logr/logr" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + "github.com/telekom/controlplane/gateway/internal/features" +) + +// Canonical Envoy filter names. jwtAuthnFilterName is also the dynamic-metadata +// namespace under which jwt_authn publishes the verified payload, which rbac +// reads back. +const ( + jwtAuthnFilterName = "envoy.filters.http.jwt_authn" + rbacFilterName = "envoy.filters.http.rbac" +) + +// payloadInMetadataKey is the shared second-level metadata key every provider +// writes its payload under, so the rbac principal can match azp on the path +// [payloadInMetadataKey, "azp"] regardless of which issuer verified the token. +const payloadInMetadataKey = "jwt_payload" + +// consumerMatchClaim mirrors the Kong path (plugin.ConsumerMatchClaim = "azp"): +// the JWT claim whose value identifies the calling consumer. +const consumerMatchClaim = "azp" + +var _ features.EnvoyFeature = &AccessControlFeature{} + +type AccessControlFeature struct { + priority int +} + +// InstanceAccessControlFeature is the registered AccessControl feature. Priority +// mirrors the Kong AccessControlFeature (10). +var InstanceAccessControlFeature = &AccessControlFeature{ + priority: 10, +} + +// Name implements [features.Feature]. +func (*AccessControlFeature) Name() gatewayv1.FeatureType { + return gatewayv1.FeatureTypeAccessControl +} + +// Priority implements [features.Feature]. +func (f *AccessControlFeature) Priority() int { + return f.priority +} + +// IsUsed implements [features.Feature]. AccessControl applies when the route +// declares trusted issuers (mirrors kong/feature/access_control.go:38). +func (f *AccessControlFeature) IsUsed(_ context.Context, builder features.EnvoyFeatureBuilder) bool { + route, ok := builder.GetRoute() + if !ok { + return false + } + return len(route.GetTrustedIssuers()) > 0 +} + +// Apply implements [features.Feature]. It contributes: +// - a jwt_authn filter that verifies the token and requires that its issuer is +// one of the route's trusted issuers (rejects otherwise), publishing the +// payload to dynamic metadata; and +// - unless DisableAccessControl, an rbac filter that allows the request only if +// the token's azp claim is in the consumer allow-list (default consumers plus +// the route's allowed consumers). An empty allow-list denies all traffic. +func (f *AccessControlFeature) Apply(ctx context.Context, builder features.EnvoyFeatureBuilder) error { + log := logr.FromContextOrDiscard(ctx).WithName("envoy.feature.access-control") + route, ok := builder.GetRoute() + if !ok { + return features.ErrNoRoute + } + + issuers := route.GetTrustedIssuers() + jwtCfg, err := buildJwtAuthn(issuers) + if err != nil { + return fmt.Errorf("building jwt_authn config: %w", err) + } + builder.AddHTTPFilter(jwtAuthnFilterName, jwtCfg) + log.V(1).Info("Added jwt_authn filter", "issuers", len(issuers)) + + if route.Spec.Security.DisableAccessControl { + log.V(1).Info("AccessControl disabled, skipping rbac filter") + return nil + } + + allow := consumerAllowList(route, builder.GetAllowedConsumers()) + rbacCfg, err := buildRBAC(allow) + if err != nil { + return fmt.Errorf("building rbac config: %w", err) + } + builder.AddHTTPFilter(rbacFilterName, rbacCfg) + log.V(0).Info("Configured access control", "allowedConsumers", len(allow)) + return nil +} + +// consumerAllowList collects the consumer names permitted on the route: the +// configured default consumers plus every allowed consumer that belongs to this +// specific route (mirrors kong/feature/access_control.go:59-68). Order-stable, +// deduplicated. +func consumerAllowList(route *gatewayv1.Route, allowed []*gatewayv1.ConsumeRoute) []string { + seen := map[string]struct{}{} + var out []string + add := func(name string) { + if _, dup := seen[name]; dup { + return + } + seen[name] = struct{}{} + out = append(out, name) + } + + for _, dc := range route.Spec.Security.DefaultConsumers { + add(dc) + } + for _, consumer := range allowed { + if consumer.Spec.Route.Equals(route) { + add(consumer.Spec.ConsumerName) + } + } + return out +} + +// buildJwtAuthn builds a JwtAuthentication that accepts a token from any of the +// trusted issuers (requires_any over one provider per issuer) and rejects +// missing/invalid tokens. Every provider publishes its payload under the shared +// payloadInMetadataKey so rbac can match azp uniformly. +// +// ponytail: remote JWKS URI is derived as /protocol/openid-connect/certs +// (Keycloak convention) and fetched via a per-issuer cluster named +// "-jwks". Those clusters are not emitted yet — wire them into the +// bundle (or switch to a shared discovery cluster) before this serves traffic. +func buildJwtAuthn(issuers []string) (*anypb.Any, error) { + providers := make(map[string]*jwtauthnv3.JwtProvider, len(issuers)) + anyReqs := make([]*jwtauthnv3.JwtRequirement, 0, len(issuers)) + + for i, issuer := range issuers { + name := fmt.Sprintf("provider-%d", i) + providers[name] = &jwtauthnv3.JwtProvider{ + Issuer: issuer, + Forward: true, + PayloadInMetadata: payloadInMetadataKey, + JwksSourceSpecifier: &jwtauthnv3.JwtProvider_RemoteJwks{ + RemoteJwks: &jwtauthnv3.RemoteJwks{ + HttpUri: &corev3.HttpUri{ + Uri: issuer + "/protocol/openid-connect/certs", + Timeout: durationpb.New(5 * time.Second), + HttpUpstreamType: &corev3.HttpUri_Cluster{Cluster: name + "-jwks"}, + }, + CacheDuration: durationpb.New(5 * time.Minute), + }, + }, + } + anyReqs = append(anyReqs, &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{ProviderName: name}, + }) + } + + cfg := &jwtauthnv3.JwtAuthentication{ + Providers: providers, + Rules: []*jwtauthnv3.RequirementRule{{ + Match: &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: "/"}, + }, + RequirementType: &jwtauthnv3.RequirementRule_Requires{ + Requires: &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{ + RequiresAny: &jwtauthnv3.JwtRequirementOrList{Requirements: anyReqs}, + }, + }, + }, + }}, + } + return anypb.New(cfg) +} + +// buildRBAC builds an ALLOW rbac filter whose single policy permits any request +// whose azp claim (published by jwt_authn under payloadInMetadataKey) matches one +// of the allowed consumer names. An empty allow-list yields an ALLOW rbac with no +// policies, which denies all requests (mirrors Kong's DenyAllGroup sentinel). +func buildRBAC(allowedConsumers []string) (*anypb.Any, error) { + rules := &rbacconfigv3.RBAC{ + Action: rbacconfigv3.RBAC_ALLOW, + Policies: map[string]*rbacconfigv3.Policy{}, + } + + if len(allowedConsumers) > 0 { + principals := make([]*rbacconfigv3.Principal, 0, len(allowedConsumers)) + for _, name := range allowedConsumers { + principals = append(principals, &rbacconfigv3.Principal{ + Identifier: &rbacconfigv3.Principal_Metadata{ + Metadata: &matcherv3.MetadataMatcher{ + Filter: jwtAuthnFilterName, + Path: []*matcherv3.MetadataMatcher_PathSegment{ + {Segment: &matcherv3.MetadataMatcher_PathSegment_Key{Key: payloadInMetadataKey}}, + {Segment: &matcherv3.MetadataMatcher_PathSegment_Key{Key: consumerMatchClaim}}, + }, + Value: &matcherv3.ValueMatcher{ + MatchPattern: &matcherv3.ValueMatcher_StringMatch{ + StringMatch: &matcherv3.StringMatcher{ + MatchPattern: &matcherv3.StringMatcher_Exact{Exact: name}, + }, + }, + }, + }, + }, + }) + } + rules.Policies["allow-consumers"] = &rbacconfigv3.Policy{ + Permissions: []*rbacconfigv3.Permission{ + {Rule: &rbacconfigv3.Permission_Any{Any: true}}, + }, + Principals: principals, + } + } + + return anypb.New(&rbachttpv3.RBAC{Rules: rules}) +} diff --git a/gateway/internal/features/envoy/nodehash.go b/gateway/internal/features/envoy/nodehash.go new file mode 100644 index 000000000..ed3c43231 --- /dev/null +++ b/gateway/internal/features/envoy/nodehash.go @@ -0,0 +1,60 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy + +import ( + "fmt" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + "github.com/go-logr/logr" +) + +// nodeMetadataRoleKey is the node.metadata field that identifies which Gateway +// a connecting Envoy belongs to. All pods of one Gateway Deployment report the +// same role, so they share a single snapshot. Envoy's bootstrap must set this. +const nodeMetadataRoleKey = "role" + +// fallbackNodeID keys the snapshot for nodes that do not report a role. They all +// share one entry rather than each spawning a distinct (empty) snapshot. +const fallbackNodeID = "unknown-role" + +var _ cachev3.NodeHash = nodeHash{} + +// nodeHash keys the snapshot cache on the Gateway identity (node.metadata.role), +// NOT the per-pod node.id. This makes all 100+ pods of a Gateway share one +// snapshot, so a change is computed and pushed once and fanned out to all +// connected streams. +type nodeHash struct{} + +// ID implements [cachev3.NodeHash]. +func (nodeHash) ID(node *corev3.Node) string { + if node == nil { + return fallbackNodeID + } + fields := node.GetMetadata().GetFields() + if role, ok := fields[nodeMetadataRoleKey]; ok { + if s := role.GetStringValue(); s != "" { + return s + } + } + return fallbackNodeID +} + +// newCacheLogger adapts a logr.Logger to the go-control-plane cache logger. +func newCacheLogger(logger logr.Logger) cacheLogger { + return cacheLogger{log: logger.WithName("envoy.cache")} +} + +type cacheLogger struct { + log logr.Logger +} + +func (l cacheLogger) Debugf(format string, args ...any) { + l.log.V(1).Info(fmt.Sprintf(format, args...)) +} +func (l cacheLogger) Infof(format string, args ...any) { l.log.V(1).Info(fmt.Sprintf(format, args...)) } +func (l cacheLogger) Warnf(format string, args ...any) { l.log.Info(fmt.Sprintf(format, args...)) } +func (l cacheLogger) Errorf(format string, args ...any) { l.log.Info(fmt.Sprintf(format, args...)) } diff --git a/gateway/internal/features/envoy/routing.go b/gateway/internal/features/envoy/routing.go new file mode 100644 index 000000000..f1bbac331 --- /dev/null +++ b/gateway/internal/features/envoy/routing.go @@ -0,0 +1,180 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy + +import ( + "fmt" + "time" + + clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + routerv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" +) + +// listenPort is the downstream port the generated HTTP listener binds to. +// ponytail: fixed port for the POC; the Gateway CRD has no listen-port field. +// Add a spec field / per-gateway config when multiple listeners are needed. +const listenPort uint32 = 10000 + +// routerFilterName / hcmFilterName are the canonical Envoy filter names. +const ( + hcmFilterName = "envoy.filters.network.http_connection_manager" + routerFilterName = "envoy.filters.http.router" + tlsTransportName = "envoy.transport_sockets.tls" +) + +// renderCoreRouting turns a Route and its resolved upstream into the core-routing +// xDS resources: one Listener (HCM + RDS via ADS), one RouteConfiguration +// (VirtualHost with one Route per path), and one Cluster (STRICT_DNS to the +// upstream target). This is the backend-agnostic equivalent of Kong's +// CreateOrReplaceRoute (Service + Route) — see kong/builder.go:243. +func renderCoreRouting(route *gatewayv1.Route, upstream gatewayv1.Upstream, httpFilters []*hcmv3.HttpFilter) (ResourceBundle, error) { + name := route.Name + routeConfigName := name + "-routes" + clusterName := name + "-cluster" + + listener, err := buildListener(name, routeConfigName, httpFilters) + if err != nil { + return ResourceBundle{}, fmt.Errorf("building listener: %w", err) + } + + return ResourceBundle{ + Listeners: []*listenerv3.Listener{listener}, + Routes: []*routev3.RouteConfiguration{buildRouteConfig(routeConfigName, route.GetHostnames(), route.GetPaths(), clusterName)}, + Clusters: []*clusterv3.Cluster{buildCluster(clusterName, upstream)}, + }, nil +} + +// buildListener creates an HTTP listener whose connection manager resolves its +// routes over RDS via ADS (same snapshot cache). Feature-contributed httpFilters +// are inserted in order before the terminal router filter (router must be last). +func buildListener(name, routeConfigName string, extraFilters []*hcmv3.HttpFilter) (*listenerv3.Listener, error) { + router, err := anypb.New(&routerv3.Router{}) + if err != nil { + return nil, fmt.Errorf("marshaling router filter: %w", err) + } + + httpFilters := make([]*hcmv3.HttpFilter, 0, len(extraFilters)+1) + httpFilters = append(httpFilters, extraFilters...) + httpFilters = append(httpFilters, &hcmv3.HttpFilter{ + Name: routerFilterName, + ConfigType: &hcmv3.HttpFilter_TypedConfig{TypedConfig: router}, + }) + + hcm := &hcmv3.HttpConnectionManager{ + CodecType: hcmv3.HttpConnectionManager_AUTO, + StatPrefix: name, + RouteSpecifier: &hcmv3.HttpConnectionManager_Rds{ + Rds: &hcmv3.Rds{ + RouteConfigName: routeConfigName, + ConfigSource: &corev3.ConfigSource{ + ResourceApiVersion: corev3.ApiVersion_V3, + ConfigSourceSpecifier: &corev3.ConfigSource_Ads{Ads: &corev3.AggregatedConfigSource{}}, + }, + }, + }, + HttpFilters: httpFilters, + } + hcmAny, err := anypb.New(hcm) + if err != nil { + return nil, fmt.Errorf("marshaling http_connection_manager: %w", err) + } + + return &listenerv3.Listener{ + Name: name, + Address: &corev3.Address{Address: &corev3.Address_SocketAddress{ + SocketAddress: &corev3.SocketAddress{ + Address: "0.0.0.0", + PortSpecifier: &corev3.SocketAddress_PortValue{PortValue: listenPort}, + }, + }}, + FilterChains: []*listenerv3.FilterChain{{ + Filters: []*listenerv3.Filter{{ + Name: hcmFilterName, + ConfigType: &listenerv3.Filter_TypedConfig{TypedConfig: hcmAny}, + }}, + }}, + }, nil +} + +// buildRouteConfig maps hostnames -> VirtualHost.domains (empty = ["*"]) and each +// path prefix -> one Envoy Route to the cluster. Empty paths = prefix "/". +func buildRouteConfig(name string, hostnames, paths []string, clusterName string) *routev3.RouteConfiguration { + domains := hostnames + if len(domains) == 0 { + domains = []string{"*"} + } + if len(paths) == 0 { + paths = []string{"/"} + } + + routes := make([]*routev3.Route, 0, len(paths)) + for _, prefix := range paths { + routes = append(routes, &routev3.Route{ + Match: &routev3.RouteMatch{ + PathSpecifier: &routev3.RouteMatch_Prefix{Prefix: prefix}, + }, + Action: &routev3.Route_Route{Route: &routev3.RouteAction{ + ClusterSpecifier: &routev3.RouteAction_Cluster{Cluster: clusterName}, + }}, + }) + } + + return &routev3.RouteConfiguration{ + Name: name, + VirtualHosts: []*routev3.VirtualHost{{ + Name: name + "-vh", + Domains: domains, + Routes: routes, + }}, + } +} + +// buildCluster creates a STRICT_DNS cluster with the single upstream target as an +// inline endpoint. https upstreams get an UpstreamTlsContext (SNI = hostname). +// ponytail: inline endpoint, no EDS, no server-cert validation. Add a validation +// context and EDS when upstream identity/health must be verified. +func buildCluster(name string, upstream gatewayv1.Upstream) *clusterv3.Cluster { + c := &clusterv3.Cluster{ + Name: name, + ConnectTimeout: durationpb.New(5 * time.Second), + ClusterDiscoveryType: &clusterv3.Cluster_Type{Type: clusterv3.Cluster_STRICT_DNS}, + LbPolicy: clusterv3.Cluster_ROUND_ROBIN, + LoadAssignment: &endpointv3.ClusterLoadAssignment{ + ClusterName: name, + Endpoints: []*endpointv3.LocalityLbEndpoints{{ + LbEndpoints: []*endpointv3.LbEndpoint{{ + HostIdentifier: &endpointv3.LbEndpoint_Endpoint{Endpoint: &endpointv3.Endpoint{ + Address: &corev3.Address{Address: &corev3.Address_SocketAddress{ + SocketAddress: &corev3.SocketAddress{ + Address: upstream.GetHostname(), + PortSpecifier: &corev3.SocketAddress_PortValue{PortValue: uint32(upstream.GetPort())}, + }, + }}, + }}, + }}, + }}, + }, + } + + if upstream.GetScheme() == "https" { + tlsCtx, _ := anypb.New(&tlsv3.UpstreamTlsContext{Sni: upstream.GetHostname()}) + c.TransportSocket = &corev3.TransportSocket{ + Name: tlsTransportName, + ConfigType: &corev3.TransportSocket_TypedConfig{TypedConfig: tlsCtx}, + } + } + + return c +} diff --git a/gateway/internal/features/envoy/server.go b/gateway/internal/features/envoy/server.go new file mode 100644 index 000000000..a24c5c542 --- /dev/null +++ b/gateway/internal/features/envoy/server.go @@ -0,0 +1,101 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy + +import ( + "context" + "fmt" + "net" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + discoveryv3 "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + serverv3 "github.com/envoyproxy/go-control-plane/pkg/server/v3" + "github.com/go-logr/logr" + "google.golang.org/grpc" + "sigs.k8s.io/controller-runtime/pkg/manager" +) + +// Server is the xDS-serving component of the control plane. +// +// Unlike the reconcilers, it runs on EVERY replica (not just the leader) so that +// each replica can serve its own connected Envoy pods from the shared in-memory +// snapshot cache. It therefore opts out of leader election via +// NeedLeaderElection() == false and is registered on the manager with +// mgr.Add(...). K8s writes (status, finalizers) stay on the leader-gated +// reconcilers; this component only serves an in-memory projection, so running it +// everywhere causes no API-server contention. +// +// ponytail: the cache is currently populated by the leader-gated route handler +// only. The all-replicas informer-driven snapshot builder (so every replica +// populates its own cache) is deferred until the EnvoyFeatureBuilder produces +// real resources — see internal/features/envoy/README.md ("CP HA & scale"). +type Server struct { + cache cachev3.Cache + addr string +} + +var ( + _ manager.Runnable = (*Server)(nil) + _ manager.LeaderElectionRunnable = (*Server)(nil) +) + +// NewServer constructs the ADS xDS server serving from the shared cache on addr. +func NewServer(cache cachev3.Cache, addr string) *Server { + return &Server{cache: cache, addr: addr} +} + +// NeedLeaderElection implements [manager.LeaderElectionRunnable]. +// +// Always false: the xDS server runs on all replicas, not only the leader. +func (*Server) NeedLeaderElection() bool { + return false +} + +// Start implements [manager.Runnable]. It serves the ADS gRPC API until ctx is +// cancelled, then stops gracefully. +func (s *Server) Start(ctx context.Context) error { + log := logr.FromContextOrDiscard(ctx).WithName("envoy.xds-server") + + srv := serverv3.NewServer(ctx, s.cache, newServerCallbacks(log)) + grpcServer := grpc.NewServer() + discoveryv3.RegisterAggregatedDiscoveryServiceServer(grpcServer, srv) + + lis, err := net.Listen("tcp", s.addr) + if err != nil { + return fmt.Errorf("listening on %q: %w", s.addr, err) + } + + errCh := make(chan error, 1) + go func() { + log.Info("Envoy xDS server listening", "addr", s.addr) + errCh <- grpcServer.Serve(lis) + }() + + select { + case <-ctx.Done(): + log.Info("Envoy xDS server stopping") + grpcServer.GracefulStop() + return nil + case err := <-errCh: + if err != nil { + return fmt.Errorf("xDS server serve: %w", err) + } + return nil + } +} + +// newServerCallbacks returns callbacks that log stream lifecycle at debug level. +func newServerCallbacks(log logr.Logger) serverv3.CallbackFuncs { + return serverv3.CallbackFuncs{ + StreamOpenFunc: func(_ context.Context, id int64, typeURL string) error { + log.V(1).Info("xDS stream opened", "streamID", id, "typeURL", typeURL) + return nil + }, + StreamClosedFunc: func(id int64, node *corev3.Node) { + log.V(1).Info("xDS stream closed", "streamID", id, "nodeID", node.GetId()) + }, + } +} diff --git a/gateway/internal/features/envoy/suite_test.go b/gateway/internal/features/envoy/suite_test.go new file mode 100644 index 000000000..9ee2be2a1 --- /dev/null +++ b/gateway/internal/features/envoy/suite_test.go @@ -0,0 +1,17 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestEnvoy(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Envoy Suite") +} diff --git a/gateway/internal/features/envoy/xds.go b/gateway/internal/features/envoy/xds.go new file mode 100644 index 000000000..95d9dbfb8 --- /dev/null +++ b/gateway/internal/features/envoy/xds.go @@ -0,0 +1,163 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy + +import ( + "context" + "fmt" + "hash/fnv" + "sort" + "sync" + + clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + endpointv3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3" + listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + cachetypes "github.com/envoyproxy/go-control-plane/pkg/cache/types" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "github.com/go-logr/logr" + "google.golang.org/protobuf/proto" +) + +// ResourceBundle is the full set of xDS resources for a single node (Gateway). +// It is a snapshot: all resources are pushed together so that RDS/CDS/EDS stay +// mutually consistent. +type ResourceBundle struct { + Listeners []*listenerv3.Listener + Clusters []*clusterv3.Cluster + Routes []*routev3.RouteConfiguration + Endpoints []*endpointv3.ClusterLoadAssignment +} + +// XdsClient publishes an xDS snapshot for a node into the underlying cache. +type XdsClient interface { + // SetSnapshotFor publishes the bundle for the given node. It is a no-op if + // the bundle content is identical to the last published snapshot for that + // node (hash-diff gate), so unrelated reconciles do not force connected + // Envoy proxies to re-ACK an unchanged snapshot. + SetSnapshotFor(ctx context.Context, nodeID string, bundle ResourceBundle) error +} + +var _ XdsClient = (*XdsCache)(nil) + +// XdsCache is the shared, long-lived xDS snapshot cache. One instance is created +// at process start; the ADS [Server] serves from it and reconcilers publish into +// it. It must be shared, not created per reconcile. +type XdsCache struct { + cache cachev3.SnapshotCache + + mu sync.Mutex + lastVersions map[string]string +} + +// NewXdsCache creates the shared snapshot cache. ADS mode is enabled so Envoy +// receives resources in dependency order (CDS before EDS, LDS before RDS). +func NewXdsCache(logger logr.Logger) *XdsCache { + return &XdsCache{ + cache: cachev3.NewSnapshotCache(true, nodeHash{}, newCacheLogger(logger)), + lastVersions: map[string]string{}, + } +} + +// Cache exposes the underlying cache so the ADS [Server] can serve from the +// same instance that reconcilers publish into. +func (c *XdsCache) Cache() cachev3.Cache { + return c.cache +} + +// SetSnapshotFor implements [XdsClient]. +func (c *XdsCache) SetSnapshotFor(ctx context.Context, nodeID string, bundle ResourceBundle) error { + log := logr.FromContextOrDiscard(ctx).WithName("envoy.xds-cache").WithValues("nodeID", nodeID) + + resources := bundle.toResourceMap() + version := hashResources(resources) + + c.mu.Lock() + unchanged := c.lastVersions[nodeID] == version + c.mu.Unlock() + + if unchanged { + log.V(1).Info("Snapshot unchanged, skipping push", "version", version) + return nil + } + + snapshot, err := cachev3.NewSnapshot(version, resources) + if err != nil { + return fmt.Errorf("creating snapshot for node %q: %w", nodeID, err) + } + + if err := c.cache.SetSnapshot(ctx, nodeID, snapshot); err != nil { + return fmt.Errorf("setting snapshot for node %q: %w", nodeID, err) + } + + c.mu.Lock() + c.lastVersions[nodeID] = version + c.mu.Unlock() + + log.Info("Published snapshot", "version", version, + "listeners", len(bundle.Listeners), "clusters", len(bundle.Clusters), + "routes", len(bundle.Routes), "endpoints", len(bundle.Endpoints)) + return nil +} + +// toResourceMap converts the bundle into the type-keyed map the cache expects. +func (b ResourceBundle) toResourceMap() map[resourcev3.Type][]cachetypes.Resource { + m := map[resourcev3.Type][]cachetypes.Resource{} + if len(b.Listeners) > 0 { + m[resourcev3.ListenerType] = toResources(b.Listeners) + } + if len(b.Clusters) > 0 { + m[resourcev3.ClusterType] = toResources(b.Clusters) + } + if len(b.Routes) > 0 { + m[resourcev3.RouteType] = toResources(b.Routes) + } + if len(b.Endpoints) > 0 { + m[resourcev3.EndpointType] = toResources(b.Endpoints) + } + return m +} + +func toResources[T cachetypes.Resource](in []T) []cachetypes.Resource { + out := make([]cachetypes.Resource, len(in)) + for i, r := range in { + out[i] = r + } + return out +} + +// hashResources computes a deterministic content hash of all resources. The +// same content always yields the same version, which gives restart and +// cross-replica determinism (Envoy sees no change if content is identical). +// +// Each resource is marshaled deterministically and hashed; the per-resource +// hashes are combined by XOR so the result is independent of ordering within a +// type. The resulting version is stable across process restarts. +func hashResources(resources map[resourcev3.Type][]cachetypes.Resource) string { + // Sort type URLs for a stable walk order. + typeURLs := make([]string, 0, len(resources)) + for t := range resources { + typeURLs = append(typeURLs, t) + } + sort.Strings(typeURLs) + + marshal := proto.MarshalOptions{Deterministic: true} + var combined uint64 + for _, t := range typeURLs { + for _, r := range resources[t] { + h := fnv.New64a() + _, _ = h.Write([]byte(t)) + if msg, ok := r.(proto.Message); ok { + b, err := marshal.Marshal(msg) + if err == nil { + _, _ = h.Write(b) + } + } + combined ^= h.Sum64() + } + } + return fmt.Sprintf("%016x", combined) +} diff --git a/gateway/internal/features/envoy/xds_test.go b/gateway/internal/features/envoy/xds_test.go new file mode 100644 index 000000000..f4e2455e4 --- /dev/null +++ b/gateway/internal/features/envoy/xds_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package envoy_test + +import ( + "context" + + clusterv3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" + cachev3 "github.com/envoyproxy/go-control-plane/pkg/cache/v3" + resourcev3 "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/telekom/controlplane/gateway/internal/features/envoy" +) + +var _ = Describe("XdsCache", func() { + const nodeID = "test-gateway" + + var ( + ctx context.Context + xds *envoy.XdsCache + cache cachev3.SnapshotCache + ) + + BeforeEach(func() { + ctx = context.Background() + xds = envoy.NewXdsCache(GinkgoLogr) + var ok bool + cache, ok = xds.Cache().(cachev3.SnapshotCache) + Expect(ok).To(BeTrue()) + }) + + versionOf := func() string { + snap, err := cache.GetSnapshot(nodeID) + Expect(err).NotTo(HaveOccurred()) + return snap.GetVersion(resourcev3.ClusterType) + } + + bundleWith := func(clusterName string) envoy.ResourceBundle { + return envoy.ResourceBundle{ + Clusters: []*clusterv3.Cluster{{Name: clusterName}}, + } + } + + It("publishes a snapshot for a node", func() { + Expect(xds.SetSnapshotFor(ctx, nodeID, bundleWith("a"))).To(Succeed()) + + snap, err := cache.GetSnapshot(nodeID) + Expect(err).NotTo(HaveOccurred()) + Expect(snap.GetResources(resourcev3.ClusterType)).To(HaveKey("a")) + }) + + It("does not re-publish when the bundle is unchanged (hash-diff gate)", func() { + Expect(xds.SetSnapshotFor(ctx, nodeID, bundleWith("a"))).To(Succeed()) + v1 := versionOf() + + Expect(xds.SetSnapshotFor(ctx, nodeID, bundleWith("a"))).To(Succeed()) + v2 := versionOf() + + Expect(v2).To(Equal(v1), "identical content must yield the same version") + }) + + It("re-publishes with a new version when the bundle changes", func() { + Expect(xds.SetSnapshotFor(ctx, nodeID, bundleWith("a"))).To(Succeed()) + v1 := versionOf() + + Expect(xds.SetSnapshotFor(ctx, nodeID, bundleWith("b"))).To(Succeed()) + v2 := versionOf() + + Expect(v2).NotTo(Equal(v1), "changed content must yield a different version") + }) +}) diff --git a/gateway/internal/features/errors.go b/gateway/internal/features/errors.go new file mode 100644 index 000000000..8b7c0a77f --- /dev/null +++ b/gateway/internal/features/errors.go @@ -0,0 +1,12 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package features + +import ( + "fmt" +) + +var ErrNoRoute = fmt.Errorf("no route found in builder context") +var ErrNoConsumer = fmt.Errorf("no consumer found in builder context") diff --git a/gateway/internal/features/interfaces.go b/gateway/internal/features/interfaces.go new file mode 100644 index 000000000..f55ff103d --- /dev/null +++ b/gateway/internal/features/interfaces.go @@ -0,0 +1,82 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package features + +import ( + "context" + + "google.golang.org/protobuf/types/known/anypb" + + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" + + "github.com/telekom/controlplane/gateway/pkg/kong/client" + "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" +) + +type FeatureBuilder interface { + GetRoute() (*gatewayv1.Route, bool) + GetConsumer() (*gatewayv1.Consumer, bool) + GetGateway() *gatewayv1.Gateway + GetAllowedConsumers() []*gatewayv1.ConsumeRoute + AddAllowedConsumers(...*gatewayv1.ConsumeRoute) + + Build(context.Context) error + BuildForConsumer(context.Context) error +} + +type FeatureInfo interface { + // Name of the feature + Name() gatewayv1.FeatureType + // Priority of this feature in the feature-chain + // The higher the priority value, the later the feature is applied. Or, in other words, the lower the priority value, the earlier the feature is applied. + // Features can have a relative priority to other features to indicate that some features should be applied before or after others. + Priority() int +} + +type Feature[T FeatureBuilder] interface { + FeatureInfo + + // IsUsed checks if the feature is already used in the current builder-context before applying it. + // The business-context (context.Context) is available to check/create runtime components such as the environment or loggers. + IsUsed(ctx context.Context, builder T) bool + // Apply applies the feature to the current builder-context + // It may modify the plugins and upstream of the builder-context + // The business-context (context.Context) is available to check/create runtime components such as the environment or loggers. + Apply(ctx context.Context, builder T) error +} + +type KongFeature = Feature[KongFeatureBuilder] + +type KongFeatureBuilder interface { + FeatureBuilder + + EnableFeature(f KongFeature) + + SetUpstream(client.Upstream) + RequestTransformerPlugin() *plugin.RequestTransformerPlugin + AclPlugin() *plugin.AclPlugin + JwtPlugin() *plugin.JwtPlugin + RateLimitPluginRoute() *plugin.RateLimitPlugin + RateLimitPluginConsumeRoute(*gatewayv1.ConsumeRoute) *plugin.RateLimitPlugin + JumperConfig() *plugin.JumperConfig + RoutingConfigs() *plugin.RoutingConfigs + IpRestrictionPlugin() *plugin.IpRestrictionPlugin + + GetKongClient() client.KongClient +} + +type EnvoyFeature = Feature[EnvoyFeatureBuilder] + +type EnvoyFeatureBuilder interface { + FeatureBuilder + + EnableFeature(f EnvoyFeature) + + // AddHTTPFilter contributes one HTTP filter (typed config marshaled to Any) + // to the listener's filter chain. Features add filters in the order their + // Apply runs (feature priority order); the builder inserts them before the + // terminal router filter. name must be the canonical Envoy filter name. + AddHTTPFilter(name string, typedConfig *anypb.Any) +} diff --git a/gateway/internal/features/builder.go b/gateway/internal/features/kong/builder.go similarity index 71% rename from gateway/internal/features/builder.go rename to gateway/internal/features/kong/builder.go index 97d5829da..d8e21ffbc 100644 --- a/gateway/internal/features/builder.go +++ b/gateway/internal/features/kong/builder.go @@ -2,64 +2,21 @@ // // SPDX-License-Identifier: Apache-2.0 -package features +package kong import ( "context" - "sort" "github.com/go-logr/logr" "github.com/pkg/errors" - gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" - + "github.com/telekom/controlplane/gateway/internal/features" "github.com/telekom/controlplane/gateway/pkg/kong/client" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" -) -var ErrNoRoute = errors.New("no route found in builder context") -var ErrNoConsumer = errors.New("no consumer found in builder context") - -type Feature interface { - // Name of the feature - Name() gatewayv1.FeatureType - // Priority of this feature in the feature-chain - // The higher the priority value, the later the feature is applied. Or, in other words, the lower the priority value, the earlier the feature is applied. - // Features can have a relative priority to other features to indicate that some features should be applied before or after others. - Priority() int - // IsUsed checks if the feature is already used in the current builder-context before applying it. - // The business-context (context.Context) is available to check/create runtime components such as the environment or loggers. - IsUsed(ctx context.Context, builder FeaturesBuilder) bool - // Apply applies the feature to the current builder-context - // It may modify the plugins and upstream of the builder-context - // The business-context (context.Context) is available to check/create runtime components such as the environment or loggers. - Apply(ctx context.Context, builder FeaturesBuilder) error -} - -type FeaturesBuilder interface { - EnableFeature(f Feature) - GetRoute() (*gatewayv1.Route, bool) - GetConsumer() (*gatewayv1.Consumer, bool) - GetGateway() *gatewayv1.Gateway - GetAllowedConsumers() []*gatewayv1.ConsumeRoute - AddAllowedConsumers(...*gatewayv1.ConsumeRoute) - - SetUpstream(client.Upstream) - RequestTransformerPlugin() *plugin.RequestTransformerPlugin - AclPlugin() *plugin.AclPlugin - JwtPlugin() *plugin.JwtPlugin - RateLimitPluginRoute() *plugin.RateLimitPlugin - RateLimitPluginConsumeRoute(*gatewayv1.ConsumeRoute) *plugin.RateLimitPlugin - JumperConfig() *plugin.JumperConfig - RoutingConfigs() *plugin.RoutingConfigs - IpRestrictionPlugin() *plugin.IpRestrictionPlugin - - Build(context.Context) error - BuildForConsumer(context.Context) error - - GetKongClient() client.KongClient -} + gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" +) -var _ FeaturesBuilder = &Builder{} +var _ features.KongFeatureBuilder = &Builder{} type Builder struct { // kc is the Kong client used to interact with the Kong Gateway @@ -85,14 +42,14 @@ type Builder struct { routingConfigs *plugin.RoutingConfigs // Features that are enabled for this builder - Features map[gatewayv1.FeatureType]Feature + Features map[gatewayv1.FeatureType]features.KongFeature } func (b *Builder) GetKongClient() client.KongClient { return b.kc } -var NewFeatureBuilder = func(kc client.KongClient, route *gatewayv1.Route, consumer *gatewayv1.Consumer, gateway *gatewayv1.Gateway) FeaturesBuilder { +var NewFeatureBuilder = func(kc client.KongClient, route *gatewayv1.Route, consumer *gatewayv1.Consumer, gateway *gatewayv1.Gateway) features.KongFeatureBuilder { return &Builder{ kc: kc, @@ -102,11 +59,11 @@ var NewFeatureBuilder = func(kc client.KongClient, route *gatewayv1.Route, consu Gateway: gateway, Plugins: map[string]client.CustomPlugin{}, - Features: map[gatewayv1.FeatureType]Feature{}, + Features: map[gatewayv1.FeatureType]features.KongFeature{}, } } -func (b *Builder) EnableFeature(f Feature) { +func (b *Builder) EnableFeature(f features.KongFeature) { b.Features[f.Name()] = f } @@ -253,10 +210,10 @@ func (b *Builder) SetUpstream(upstream client.Upstream) { func (b *Builder) Build(ctx context.Context) error { log := logr.FromContextOrDiscard(ctx).WithName("features.builder").WithValues("route", b.Route.Name) if b.Route == nil { - return ErrNoRoute + return features.ErrNoRoute } - for _, f := range sortFeatures(toSlice(b.Features)) { + for _, f := range features.SortFeatures(features.ToSlice(b.Features)) { if f.IsUsed(ctx, b) { log.V(1).Info("Applying feature", "name", f.Name()) err := f.Apply(ctx, b) @@ -295,7 +252,7 @@ func (b *Builder) Build(ctx context.Context) error { } } - err = b.kc.CleanupPlugins(ctx, b.Route, nil, toSlice(b.Plugins)) + err = b.kc.CleanupPlugins(ctx, b.Route, nil, features.ToSlice(b.Plugins)) if err != nil { return errors.Wrap(err, "failed to cleanup plugins") } @@ -306,10 +263,10 @@ func (b *Builder) Build(ctx context.Context) error { func (b *Builder) BuildForConsumer(ctx context.Context) error { log := logr.FromContextOrDiscard(ctx).WithName("features.builder").WithValues("consumer", b.Consumer.Name) if b.Consumer == nil { - return ErrNoConsumer + return features.ErrNoConsumer } - for _, f := range sortFeatures(toSlice(b.Features)) { + for _, f := range features.SortFeatures(features.ToSlice(b.Features)) { if f.IsUsed(ctx, b) { log.V(1).Info("Applying feature", "name", f.Name()) err := f.Apply(ctx, b) @@ -336,7 +293,7 @@ func (b *Builder) BuildForConsumer(ctx context.Context) error { } } - err = b.kc.CleanupPlugins(ctx, nil, b.Consumer, toSlice(b.Plugins)) + err = b.kc.CleanupPlugins(ctx, nil, b.Consumer, features.ToSlice(b.Plugins)) if err != nil { return errors.Wrap(err, "failed to cleanup plugins") } @@ -344,21 +301,3 @@ func (b *Builder) BuildForConsumer(ctx context.Context) error { return nil } - -// sort features based on their priority -// the higher the priority, the later the feature is applied -// this is important because some features might depend on other features -func sortFeatures(featureList []Feature) []Feature { - sort.Slice(featureList, func(i, j int) bool { - return featureList[i].Priority() < featureList[j].Priority() - }) - return featureList -} - -func toSlice[K comparable, T any](m map[K]T) []T { - s := make([]T, 0, len(m)) - for _, v := range m { - s = append(s, v) - } - return s -} diff --git a/gateway/internal/features/builder_test.go b/gateway/internal/features/kong/builder_test.go similarity index 81% rename from gateway/internal/features/builder_test.go rename to gateway/internal/features/kong/builder_test.go index d000caa2f..5d4ec3c2a 100644 --- a/gateway/internal/features/builder_test.go +++ b/gateway/internal/features/kong/builder_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package features_test +package kong_test import ( "context" @@ -15,6 +15,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" + "github.com/telekom/controlplane/gateway/internal/features/kong" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client" clientmock "github.com/telekom/controlplane/gateway/pkg/kong/client/mock" @@ -53,12 +54,12 @@ var _ = Describe("Builder", func() { Describe("Build()", func() { Context("happy path", func() { It("sorts and applies features, creates route+plugins, and calls cleanup", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("test-feature")).Maybe() mockFeature.EXPECT().Priority().Return(10).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) @@ -66,7 +67,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplacePlugin(mock.Anything, mock.Anything).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) // Trigger plugin creation so at least one plugin exists @@ -79,25 +80,25 @@ var _ = Describe("Builder", func() { Context("when a feature returns IsUsed=false", func() { It("skips that feature without calling Apply", func() { - skippedFeature := featmock.NewMockFeature(GinkgoT()) + skippedFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) skippedFeature.EXPECT().Name().Return(gatewayv1.FeatureType("skipped")).Maybe() skippedFeature.EXPECT().Priority().Return(5).Maybe() skippedFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(false) // Apply should NOT be called - usedFeature := featmock.NewMockFeature(GinkgoT()) + usedFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) usedFeature.EXPECT().Name().Return(gatewayv1.FeatureType("used")).Maybe() usedFeature.EXPECT().Priority().Return(10).Maybe() usedFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) usedFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(skippedFeature) builder.EnableFeature(usedFeature) @@ -110,13 +111,13 @@ var _ = Describe("Builder", func() { It("propagates the error and stops the pipeline", func() { applyErr := errors.New("feature apply failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("failing")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything).Return(applyErr) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -126,14 +127,14 @@ var _ = Describe("Builder", func() { Context("when no upstream is set after features", func() { It("returns an error indicating upstream is not set", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("no-upstream")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything).Return(nil) // Feature does NOT set an upstream - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -146,7 +147,7 @@ var _ = Describe("Builder", func() { It("panics due to nil dereference before the guard check", func() { // NOTE: Build() accesses b.Route.Name for logging before the nil check, // so a nil route causes a panic rather than returning ErrNoRoute. - builder := features.NewFeatureBuilder(mockKC, nil, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, nil, gateway) Expect(func() { _ = builder.Build(ctx) //nolint:errcheck }).To(Panic()) @@ -155,12 +156,12 @@ var _ = Describe("Builder", func() { Context("when RoutingConfigs are present", func() { It("adds routing_config header to the request transformer plugin", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("routing")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) fb.RoutingConfigs().Add(&plugin.RoutingConfig{ RemoteApiUrl: "http://remote.example.com", @@ -175,7 +176,7 @@ var _ = Describe("Builder", func() { }).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -192,12 +193,12 @@ var _ = Describe("Builder", func() { Context("when JumperConfig is present (without RoutingConfigs)", func() { It("adds jumper_config header to the request transformer plugin", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("jumper")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) // Access JumperConfig to initialize it (simulating a feature that sets it) fb.JumperConfig().OAuth["test-consumer"] = plugin.OauthCredentials{ @@ -213,7 +214,7 @@ var _ = Describe("Builder", func() { }).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -227,19 +228,19 @@ var _ = Describe("Builder", func() { Context("when neither RoutingConfigs nor JumperConfig are present", func() { It("does not add routing or jumper headers", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("plain")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -251,18 +252,18 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { routeErr := errors.New("kong route creation failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("upstream-setter")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(routeErr) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -276,12 +277,12 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { pluginErr := errors.New("kong plugin creation failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("plugin-fail")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) // Access a plugin to ensure it's created in the Plugins map fb.RequestTransformerPlugin() @@ -290,7 +291,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CreateOrReplacePlugin(mock.Anything, mock.Anything).Return(nil, pluginErr) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -304,19 +305,19 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { cleanupErr := errors.New("cleanup failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("cleanup-fail")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(cleanupErr) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -344,7 +345,7 @@ var _ = Describe("Builder", func() { Context("happy path", func() { It("applies features, creates consumer and plugins, and calls cleanup", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("consumer-feature")).Maybe() mockFeature.EXPECT().Priority().Return(10).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) @@ -353,7 +354,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplaceConsumer(mock.Anything, mock.Anything).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -363,12 +364,12 @@ var _ = Describe("Builder", func() { Context("happy path with plugins", func() { It("creates plugins via CreateOrReplacePlugin", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("ip-feature")).Maybe() mockFeature.EXPECT().Priority().Return(10).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.IpRestrictionPlugin() }).Return(nil) @@ -376,7 +377,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplacePlugin(mock.Anything, mock.Anything).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -388,7 +389,7 @@ var _ = Describe("Builder", func() { It("panics due to nil dereference before the guard check", func() { // NOTE: BuildForConsumer() accesses b.Consumer.Name for logging // before the nil check, so a nil consumer causes a panic. - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) Expect(func() { _ = builder.BuildForConsumer(ctx) //nolint:errcheck }).To(Panic()) @@ -399,13 +400,13 @@ var _ = Describe("Builder", func() { It("propagates the error", func() { applyErr := errors.New("consumer feature error") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("failing-consumer")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything).Return(applyErr) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -417,7 +418,7 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { consumerErr := errors.New("kong consumer creation failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("consumer-ok")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) @@ -425,7 +426,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplaceConsumer(mock.Anything, mock.Anything).Return(nil, consumerErr) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -439,19 +440,19 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { pluginErr := errors.New("consumer plugin creation failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("consumer-plugin-fail")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.IpRestrictionPlugin() }).Return(nil) mockKC.EXPECT().CreateOrReplaceConsumer(mock.Anything, mock.Anything).Return(nil, nil) mockKC.EXPECT().CreateOrReplacePlugin(mock.Anything, mock.Anything).Return(nil, pluginErr) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -465,7 +466,7 @@ var _ = Describe("Builder", func() { It("returns a wrapped error", func() { cleanupErr := errors.New("consumer cleanup failed") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("consumer-cleanup")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) @@ -474,7 +475,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplaceConsumer(mock.Anything, mock.Anything).Return(nil, nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(cleanupErr) - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) builder.EnableFeature(mockFeature) err := builder.BuildForConsumer(ctx) @@ -489,30 +490,30 @@ var _ = Describe("Builder", func() { It("applies features in ascending priority order (lowest first)", func() { var appliedOrder []int - f0 := featmock.NewMockFeature(GinkgoT()) + f0 := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) f0.EXPECT().Name().Return(gatewayv1.FeatureType("feature-10")).Maybe() f0.EXPECT().Priority().Return(10).Maybe() f0.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) f0.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, _ features.FeaturesBuilder) { + Run(func(_ context.Context, _ features.KongFeatureBuilder) { appliedOrder = append(appliedOrder, 10) }).Return(nil) - f1 := featmock.NewMockFeature(GinkgoT()) + f1 := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) f1.EXPECT().Name().Return(gatewayv1.FeatureType("feature-0")).Maybe() f1.EXPECT().Priority().Return(0).Maybe() f1.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) f1.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, _ features.FeaturesBuilder) { + Run(func(_ context.Context, _ features.KongFeatureBuilder) { appliedOrder = append(appliedOrder, 0) }).Return(nil) - f2 := featmock.NewMockFeature(GinkgoT()) + f2 := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) f2.EXPECT().Name().Return(gatewayv1.FeatureType("feature-100")).Maybe() f2.EXPECT().Priority().Return(100).Maybe() f2.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) f2.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { appliedOrder = append(appliedOrder, 100) fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) @@ -520,7 +521,7 @@ var _ = Describe("Builder", func() { mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(f0) builder.EnableFeature(f1) builder.EnableFeature(f2) @@ -533,19 +534,19 @@ var _ = Describe("Builder", func() { Describe("EnableFeature()", func() { It("adds the feature to the builder", func() { - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("my-feature")).Maybe() mockFeature.EXPECT().Priority().Return(5).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) // The feature is used during Build, confirming it was registered @@ -554,24 +555,24 @@ var _ = Describe("Builder", func() { }) It("replaces a feature with the same name", func() { - firstFeature := featmock.NewMockFeature(GinkgoT()) + firstFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) firstFeature.EXPECT().Name().Return(gatewayv1.FeatureType("duplicate")).Maybe() firstFeature.EXPECT().Priority().Return(1).Maybe() // The first feature should NOT be called (replaced by second) - secondFeature := featmock.NewMockFeature(GinkgoT()) + secondFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) secondFeature.EXPECT().Name().Return(gatewayv1.FeatureType("duplicate")).Maybe() secondFeature.EXPECT().Priority().Return(2).Maybe() secondFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) secondFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) }).Return(nil) mockKC.EXPECT().CreateOrReplaceRoute(mock.Anything, mock.Anything, mock.Anything).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(firstFeature) builder.EnableFeature(secondFeature) // Should replace the first @@ -583,14 +584,14 @@ var _ = Describe("Builder", func() { Describe("Plugin getters (lazy initialization)", func() { Context("RequestTransformerPlugin()", func() { It("creates a new plugin on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rtp := builder.RequestTransformerPlugin() Expect(rtp).ToNot(BeNil()) Expect(rtp.GetName()).To(Equal("request-transformer")) }) It("returns the same plugin on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rtp1 := builder.RequestTransformerPlugin() rtp2 := builder.RequestTransformerPlugin() Expect(rtp1).To(BeIdenticalTo(rtp2)) @@ -599,14 +600,14 @@ var _ = Describe("Builder", func() { Context("AclPlugin()", func() { It("creates a new plugin on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) acl := builder.AclPlugin() Expect(acl).ToNot(BeNil()) Expect(acl.GetName()).To(Equal("acl")) }) It("returns the same plugin on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) acl1 := builder.AclPlugin() acl2 := builder.AclPlugin() Expect(acl1).To(BeIdenticalTo(acl2)) @@ -615,14 +616,14 @@ var _ = Describe("Builder", func() { Context("JwtPlugin()", func() { It("creates a new plugin on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) jwt := builder.JwtPlugin() Expect(jwt).ToNot(BeNil()) Expect(jwt.GetName()).To(Equal("jwt-keycloak")) }) It("returns the same plugin on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) jwt1 := builder.JwtPlugin() jwt2 := builder.JwtPlugin() Expect(jwt1).To(BeIdenticalTo(jwt2)) @@ -631,14 +632,14 @@ var _ = Describe("Builder", func() { Context("RateLimitPluginRoute()", func() { It("creates a new plugin on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rl := builder.RateLimitPluginRoute() Expect(rl).ToNot(BeNil()) Expect(rl.GetName()).To(Equal("rate-limiting-merged")) }) It("returns the same plugin on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rl1 := builder.RateLimitPluginRoute() rl2 := builder.RateLimitPluginRoute() Expect(rl1).To(BeIdenticalTo(rl2)) @@ -652,7 +653,7 @@ var _ = Describe("Builder", func() { ConsumerName: "consumer-a", }, } - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rl := builder.RateLimitPluginConsumeRoute(cr) Expect(rl).ToNot(BeNil()) Expect(rl.GetName()).To(Equal("rate-limiting-merged")) @@ -664,7 +665,7 @@ var _ = Describe("Builder", func() { ConsumerName: "consumer-b", }, } - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rl1 := builder.RateLimitPluginConsumeRoute(cr) rl2 := builder.RateLimitPluginConsumeRoute(cr) Expect(rl1).To(BeIdenticalTo(rl2)) @@ -681,7 +682,7 @@ var _ = Describe("Builder", func() { ConsumerName: "consumer-y", }, } - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rlA := builder.RateLimitPluginConsumeRoute(crA) rlB := builder.RateLimitPluginConsumeRoute(crB) Expect(rlA).ToNot(BeIdenticalTo(rlB)) @@ -696,7 +697,7 @@ var _ = Describe("Builder", func() { Namespace: "default", }, } - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) ip := builder.IpRestrictionPlugin() Expect(ip).ToNot(BeNil()) Expect(ip.GetName()).To(Equal("ip-restriction")) @@ -709,7 +710,7 @@ var _ = Describe("Builder", func() { Namespace: "default", }, } - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) ip1 := builder.IpRestrictionPlugin() ip2 := builder.IpRestrictionPlugin() Expect(ip1).To(BeIdenticalTo(ip2)) @@ -718,7 +719,7 @@ var _ = Describe("Builder", func() { Context("JumperConfig()", func() { It("creates a new config on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) jc := builder.JumperConfig() Expect(jc).ToNot(BeNil()) Expect(jc.OAuth).ToNot(BeNil()) @@ -726,7 +727,7 @@ var _ = Describe("Builder", func() { }) It("returns the same config on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) jc1 := builder.JumperConfig() jc2 := builder.JumperConfig() Expect(jc1).To(BeIdenticalTo(jc2)) @@ -735,14 +736,14 @@ var _ = Describe("Builder", func() { Context("RoutingConfigs()", func() { It("creates a new config on first call", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rc := builder.RoutingConfigs() Expect(rc).ToNot(BeNil()) Expect(rc.Len()).To(Equal(0)) }) It("returns the same config on subsequent calls", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) rc1 := builder.RoutingConfigs() rc2 := builder.RoutingConfigs() Expect(rc1).To(BeIdenticalTo(rc2)) @@ -754,12 +755,12 @@ var _ = Describe("Builder", func() { It("stores the upstream for use during Build", func() { upstream := client.NewUpstreamOrDie("http://my-service:8080/path") - mockFeature := featmock.NewMockFeature(GinkgoT()) + mockFeature := featmock.NewMockFeature[features.KongFeatureBuilder](GinkgoT()) mockFeature.EXPECT().Name().Return(gatewayv1.FeatureType("noop")).Maybe() mockFeature.EXPECT().Priority().Return(1).Maybe() mockFeature.EXPECT().IsUsed(mock.Anything, mock.Anything).Return(true) mockFeature.EXPECT().Apply(mock.Anything, mock.Anything). - Run(func(_ context.Context, fb features.FeaturesBuilder) { + Run(func(_ context.Context, fb features.KongFeatureBuilder) { fb.SetUpstream(upstream) }).Return(nil) @@ -770,7 +771,7 @@ var _ = Describe("Builder", func() { }).Return(nil) mockKC.EXPECT().CleanupPlugins(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil) - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.EnableFeature(mockFeature) err := builder.Build(ctx) @@ -781,14 +782,14 @@ var _ = Describe("Builder", func() { Describe("GetRoute()", func() { It("returns the route and true when set", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) r, ok := builder.GetRoute() Expect(ok).To(BeTrue()) Expect(r).To(Equal(route)) }) It("returns nil and false when route is not set", func() { - builder := features.NewFeatureBuilder(mockKC, nil, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, nil, gateway) r, ok := builder.GetRoute() Expect(ok).To(BeFalse()) Expect(r).To(BeNil()) @@ -800,14 +801,14 @@ var _ = Describe("Builder", func() { consumer := &gatewayv1.Consumer{ ObjectMeta: metav1.ObjectMeta{Name: "my-consumer"}, } - builder := features.NewFeatureBuilder(mockKC, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(mockKC, nil, consumer, gateway) c, ok := builder.GetConsumer() Expect(ok).To(BeTrue()) Expect(c).To(Equal(consumer)) }) It("returns nil and false when consumer is not set", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) c, ok := builder.GetConsumer() Expect(ok).To(BeFalse()) Expect(c).To(BeNil()) @@ -816,14 +817,14 @@ var _ = Describe("Builder", func() { Describe("GetGateway()", func() { It("returns the gateway", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) Expect(builder.GetGateway()).To(Equal(gateway)) }) }) Describe("GetAllowedConsumers() / AddAllowedConsumers()", func() { It("starts empty", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) Expect(builder.GetAllowedConsumers()).To(BeEmpty()) }) @@ -835,7 +836,7 @@ var _ = Describe("Builder", func() { Spec: gatewayv1.ConsumeRouteSpec{ConsumerName: "c2"}, } - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) builder.AddAllowedConsumers(cr1, cr2) Expect(builder.GetAllowedConsumers()).To(HaveLen(2)) Expect(builder.GetAllowedConsumers()).To(ContainElements(cr1, cr2)) @@ -844,7 +845,7 @@ var _ = Describe("Builder", func() { Describe("GetKongClient()", func() { It("returns the kong client", func() { - builder := features.NewFeatureBuilder(mockKC, route, nil, gateway) + builder := kong.NewFeatureBuilder(mockKC, route, nil, gateway) Expect(builder.GetKongClient()).To(Equal(mockKC)) }) }) diff --git a/gateway/internal/features/feature/access_control.go b/gateway/internal/features/kong/feature/access_control.go similarity index 93% rename from gateway/internal/features/feature/access_control.go rename to gateway/internal/features/kong/feature/access_control.go index ffdafe95f..5ca48d6fa 100644 --- a/gateway/internal/features/feature/access_control.go +++ b/gateway/internal/features/kong/feature/access_control.go @@ -12,7 +12,7 @@ import ( "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) -var _ features.Feature = &AccessControlFeature{} +var _ features.KongFeature = &AccessControlFeature{} type AccessControlFeature struct { priority int @@ -30,7 +30,7 @@ func (f *AccessControlFeature) Priority() int { return f.priority } -func (f *AccessControlFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *AccessControlFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -38,7 +38,7 @@ func (f *AccessControlFeature) IsUsed(ctx context.Context, builder features.Feat return len(route.GetTrustedIssuers()) > 0 } -func (f *AccessControlFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *AccessControlFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/access_control_test.go b/gateway/internal/features/kong/feature/access_control_test.go similarity index 97% rename from gateway/internal/features/feature/access_control_test.go rename to gateway/internal/features/kong/feature/access_control_test.go index 8e6597f48..5a1cdc889 100644 --- a/gateway/internal/features/feature/access_control_test.go +++ b/gateway/internal/features/kong/feature/access_control_test.go @@ -13,7 +13,7 @@ import ( "github.com/telekom/controlplane/common/pkg/types" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -24,13 +24,13 @@ var _ = Describe("AccessControlFeature", func() { var ( ctx context.Context f *feature.AccessControlFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceAccessControlFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/basic_auth.go b/gateway/internal/features/kong/feature/basic_auth.go similarity index 95% rename from gateway/internal/features/feature/basic_auth.go rename to gateway/internal/features/kong/feature/basic_auth.go index 63108dc1c..cf76017df 100644 --- a/gateway/internal/features/feature/basic_auth.go +++ b/gateway/internal/features/kong/feature/basic_auth.go @@ -14,7 +14,7 @@ import ( secretManagerApi "github.com/telekom/controlplane/secret-manager/api" ) -var _ features.Feature = (*BasicAuthFeature)(nil) +var _ features.KongFeature = (*BasicAuthFeature)(nil) type BasicAuthFeature struct { priority int @@ -32,7 +32,7 @@ func (b *BasicAuthFeature) Priority() int { return b.priority } -func (b *BasicAuthFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (b *BasicAuthFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { // Check if route exists route, ok := builder.GetRoute() if !ok { @@ -67,7 +67,7 @@ func (b *BasicAuthFeature) IsUsed(ctx context.Context, builder features.Features return false } -func (b *BasicAuthFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) error { +func (b *BasicAuthFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) error { jumperConfig := builder.JumperConfig() route, ok := builder.GetRoute() if !ok { diff --git a/gateway/internal/features/feature/basic_auth_test.go b/gateway/internal/features/kong/feature/basic_auth_test.go similarity index 98% rename from gateway/internal/features/feature/basic_auth_test.go rename to gateway/internal/features/kong/feature/basic_auth_test.go index 8b2427c38..d4381cb6e 100644 --- a/gateway/internal/features/feature/basic_auth_test.go +++ b/gateway/internal/features/kong/feature/basic_auth_test.go @@ -13,7 +13,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" secretManagerApi "github.com/telekom/controlplane/secret-manager/api" @@ -25,13 +25,13 @@ var _ = Describe("BasicAuthFeature", func() { var ( ctx context.Context f *feature.BasicAuthFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceBasicAuthFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/circuit_breaker.go b/gateway/internal/features/kong/feature/circuit_breaker.go similarity index 93% rename from gateway/internal/features/feature/circuit_breaker.go rename to gateway/internal/features/kong/feature/circuit_breaker.go index 58b7805b4..8bfd5fe73 100644 --- a/gateway/internal/features/feature/circuit_breaker.go +++ b/gateway/internal/features/kong/feature/circuit_breaker.go @@ -12,13 +12,13 @@ import ( "github.com/telekom/controlplane/common/pkg/util/contextutil" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature/config" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature/config" kong "github.com/telekom/controlplane/gateway/pkg/kong/api" "github.com/telekom/controlplane/gateway/pkg/kong/client" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) -var _ features.Feature = &CircuitBreakerFeature{} +var _ features.KongFeature = &CircuitBreakerFeature{} // CircuitBreakerPriority The priority is the highest of all features as it modifies the Kong service host and to make sure it will not interfere with any other feature. const CircuitBreakerPriority = 110 @@ -43,7 +43,7 @@ func (c CircuitBreakerFeature) Priority() int { } // IsUsed as per ARD0014, CB is an opt-in feature, but the cleanup mechanism is special, since CB is not a plugin - see comments inside the function -func (c CircuitBreakerFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (c CircuitBreakerFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { // assume that CB is not used if there is no route @@ -67,7 +67,7 @@ func (c CircuitBreakerFeature) IsUsed(ctx context.Context, builder features.Feat return false } -func (c CircuitBreakerFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) error { +func (c CircuitBreakerFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) error { log := logr.FromContextOrDiscard(ctx) log.V(1).Info("Configuring CircuitBreaker", "name", c.Name()) @@ -83,7 +83,7 @@ func (c CircuitBreakerFeature) Apply(ctx context.Context, builder features.Featu } } -func handleDeletion(ctx context.Context, builder features.FeaturesBuilder, route *gatewayv1.Route) error { +func handleDeletion(ctx context.Context, builder features.KongFeatureBuilder, route *gatewayv1.Route) error { // default configuration if CB is disabled builder.SetUpstream(client.NewUpstreamOrDie(plugin.LocalhostProxyUrl)) @@ -108,7 +108,7 @@ func isDeleteScenario(route *gatewayv1.Route) bool { } } -func handleApply(ctx context.Context, builder features.FeaturesBuilder, route *gatewayv1.Route) error { +func handleApply(ctx context.Context, builder features.KongFeatureBuilder, route *gatewayv1.Route) error { routeName := route.GetName() kongClient := builder.GetKongClient() kongAdminApi := kongClient.GetKongAdminApi() diff --git a/gateway/internal/features/feature/circuit_breaker_test.go b/gateway/internal/features/kong/feature/circuit_breaker_test.go similarity index 98% rename from gateway/internal/features/feature/circuit_breaker_test.go rename to gateway/internal/features/kong/feature/circuit_breaker_test.go index fba05145c..a52f048dc 100644 --- a/gateway/internal/features/feature/circuit_breaker_test.go +++ b/gateway/internal/features/kong/feature/circuit_breaker_test.go @@ -15,7 +15,7 @@ import ( "github.com/telekom/controlplane/common/pkg/util/contextutil" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" kong "github.com/telekom/controlplane/gateway/pkg/kong/api" clientmock "github.com/telekom/controlplane/gateway/pkg/kong/client/mock" @@ -27,13 +27,13 @@ var _ = Describe("CircuitBreakerFeature", func() { var ( ctx context.Context f *feature.CircuitBreakerFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = contextutil.WithEnv(context.Background(), "test-env") f = feature.InstanceCircuitBreakerFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/claims.go b/gateway/internal/features/kong/feature/claims.go similarity index 95% rename from gateway/internal/features/feature/claims.go rename to gateway/internal/features/kong/feature/claims.go index 5f4252625..e2b94fd55 100644 --- a/gateway/internal/features/feature/claims.go +++ b/gateway/internal/features/kong/feature/claims.go @@ -12,7 +12,7 @@ import ( "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) -var _ features.Feature = &ClaimsFeature{} +var _ features.KongFeature = &ClaimsFeature{} // ClaimsFeature writes provider exposure token claims into JumperConfig.Claims. // Claims land in the "default" bucket (applies to all consumers). Modeled on @@ -33,7 +33,7 @@ func (f *ClaimsFeature) Priority() int { return f.priority } -func (f *ClaimsFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *ClaimsFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -64,7 +64,7 @@ func (f *ClaimsFeature) IsUsed(ctx context.Context, builder features.FeaturesBui return isConfigured } -func (f *ClaimsFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) error { +func (f *ClaimsFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) error { jumperConfig := builder.JumperConfig() route, ok := builder.GetRoute() if !ok { diff --git a/gateway/internal/features/feature/claims_test.go b/gateway/internal/features/kong/feature/claims_test.go similarity index 98% rename from gateway/internal/features/feature/claims_test.go rename to gateway/internal/features/kong/feature/claims_test.go index a3e339299..10da59331 100644 --- a/gateway/internal/features/feature/claims_test.go +++ b/gateway/internal/features/kong/feature/claims_test.go @@ -12,7 +12,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) @@ -22,13 +22,13 @@ var _ = Describe("ClaimsFeature", func() { var ( ctx context.Context f *feature.ClaimsFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceClaimsFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/config/circuit_breaker_config.go b/gateway/internal/features/kong/feature/config/circuit_breaker_config.go similarity index 100% rename from gateway/internal/features/feature/config/circuit_breaker_config.go rename to gateway/internal/features/kong/feature/config/circuit_breaker_config.go diff --git a/gateway/internal/features/feature/custom_scopes.go b/gateway/internal/features/kong/feature/custom_scopes.go similarity index 94% rename from gateway/internal/features/feature/custom_scopes.go rename to gateway/internal/features/kong/feature/custom_scopes.go index a18a688a7..5dff0e2da 100644 --- a/gateway/internal/features/feature/custom_scopes.go +++ b/gateway/internal/features/kong/feature/custom_scopes.go @@ -13,7 +13,7 @@ import ( "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) -var _ features.Feature = &CustomScopesFeature{} +var _ features.KongFeature = &CustomScopesFeature{} type CustomScopesFeature struct { priority int @@ -31,7 +31,7 @@ func (f *CustomScopesFeature) Priority() int { return f.priority } -func (f *CustomScopesFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *CustomScopesFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -43,7 +43,7 @@ func (f *CustomScopesFeature) IsUsed(ctx context.Context, builder features.Featu return notPassThrough && (isPrimaryRoute || isFailoverSecondary) } -func (f *CustomScopesFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *CustomScopesFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { jumperConfig := builder.JumperConfig() route, ok := builder.GetRoute() if !ok { diff --git a/gateway/internal/features/feature/custom_scopes_test.go b/gateway/internal/features/kong/feature/custom_scopes_test.go similarity index 97% rename from gateway/internal/features/feature/custom_scopes_test.go rename to gateway/internal/features/kong/feature/custom_scopes_test.go index 898b6d84f..24a909269 100644 --- a/gateway/internal/features/feature/custom_scopes_test.go +++ b/gateway/internal/features/kong/feature/custom_scopes_test.go @@ -12,7 +12,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) @@ -22,13 +22,13 @@ var _ = Describe("CustomScopesFeature", func() { var ( ctx context.Context f *feature.CustomScopesFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceCustomScopesFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/dynamic_upstream.go b/gateway/internal/features/kong/feature/dynamic_upstream.go similarity index 86% rename from gateway/internal/features/feature/dynamic_upstream.go rename to gateway/internal/features/kong/feature/dynamic_upstream.go index 05e48523a..4c5aa3d00 100644 --- a/gateway/internal/features/feature/dynamic_upstream.go +++ b/gateway/internal/features/kong/feature/dynamic_upstream.go @@ -12,7 +12,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &DynamicUpstreamFeature{} +var _ features.KongFeature = &DynamicUpstreamFeature{} var InstanceDynamicUpstreamFeature = &DynamicUpstreamFeature{ // Priority is set to be higher than InstanceLastMileSecurityFeature to ensure @@ -24,18 +24,18 @@ type DynamicUpstreamFeature struct { priority int } -// Name implements features.Feature. +// Name implements features.KongFeature. func (d *DynamicUpstreamFeature) Name() gatewayv1.FeatureType { return gatewayv1.FeatureTypeDynamicUpstream } -// Priority implements features.Feature. +// Priority implements features.KongFeature. func (d *DynamicUpstreamFeature) Priority() int { return d.priority } -// IsUsed implements features.Feature. -func (d *DynamicUpstreamFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +// IsUsed implements features.KongFeature. +func (d *DynamicUpstreamFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -59,8 +59,8 @@ func (d *DynamicUpstreamFeature) IsUsed(ctx context.Context, builder features.Fe } -// Apply implements features.Feature. -func (d *DynamicUpstreamFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) error { +// Apply implements features.KongFeature. +func (d *DynamicUpstreamFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) error { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/dynamic_upstream_test.go b/gateway/internal/features/kong/feature/dynamic_upstream_test.go similarity index 97% rename from gateway/internal/features/feature/dynamic_upstream_test.go rename to gateway/internal/features/kong/feature/dynamic_upstream_test.go index 12df23316..911b458ad 100644 --- a/gateway/internal/features/feature/dynamic_upstream_test.go +++ b/gateway/internal/features/kong/feature/dynamic_upstream_test.go @@ -12,7 +12,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) @@ -22,13 +22,13 @@ var _ = Describe("DynamicUpstreamFeature", func() { var ( ctx context.Context f *feature.DynamicUpstreamFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceDynamicUpstreamFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/external_idp.go b/gateway/internal/features/kong/feature/external_idp.go similarity index 98% rename from gateway/internal/features/feature/external_idp.go rename to gateway/internal/features/kong/feature/external_idp.go index ff8e79e1a..dc0aa5d0d 100644 --- a/gateway/internal/features/feature/external_idp.go +++ b/gateway/internal/features/kong/feature/external_idp.go @@ -16,7 +16,7 @@ import ( secretManagerApi "github.com/telekom/controlplane/secret-manager/api" ) -var _ features.Feature = &ExternalIDPFeature{} +var _ features.KongFeature = &ExternalIDPFeature{} // defaultKey for provider (exposure) config. // Used as a fallback in Jumper if no consumer key is found @@ -41,7 +41,7 @@ func (f *ExternalIDPFeature) Priority() int { // IsUsed checks if the ExternalIDP feature is used in the route. // It can either be used as a primary route feature or as a failover security feature. -func (f *ExternalIDPFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *ExternalIDPFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -60,7 +60,7 @@ func (f *ExternalIDPFeature) IsUsed(ctx context.Context, builder features.Featur return !route.Spec.PassThrough && isConfigured } -func (f *ExternalIDPFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *ExternalIDPFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { rtpPlugin := builder.RequestTransformerPlugin() route, ok := builder.GetRoute() if !ok { diff --git a/gateway/internal/features/feature/external_idp_test.go b/gateway/internal/features/kong/feature/external_idp_test.go similarity index 99% rename from gateway/internal/features/feature/external_idp_test.go rename to gateway/internal/features/kong/feature/external_idp_test.go index 7425024da..b8c2b8189 100644 --- a/gateway/internal/features/feature/external_idp_test.go +++ b/gateway/internal/features/kong/feature/external_idp_test.go @@ -13,7 +13,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" secretManagerApi "github.com/telekom/controlplane/secret-manager/api" @@ -25,13 +25,13 @@ var _ = Describe("ExternalIDPFeature", func() { var ( ctx context.Context f *feature.ExternalIDPFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceExternalIDPFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/failover.go b/gateway/internal/features/kong/feature/failover.go similarity index 97% rename from gateway/internal/features/feature/failover.go rename to gateway/internal/features/kong/feature/failover.go index 6c44f8814..4ba1b7187 100644 --- a/gateway/internal/features/feature/failover.go +++ b/gateway/internal/features/kong/feature/failover.go @@ -14,7 +14,7 @@ import ( "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) -var _ features.Feature = &FailoverFeature{} +var _ features.KongFeature = &FailoverFeature{} // FailoverFeature implements the failover feature for routes. type FailoverFeature struct { @@ -33,7 +33,7 @@ func (f *FailoverFeature) Priority() int { return f.priority } -func (f *FailoverFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *FailoverFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -41,7 +41,7 @@ func (f *FailoverFeature) IsUsed(ctx context.Context, builder features.FeaturesB return route.Spec.Traffic.Failover != nil && len(route.Spec.Traffic.Failover.Targets) > 0 } -func (f *FailoverFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *FailoverFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { routingConfigs := builder.RoutingConfigs() route, ok := builder.GetRoute() if !ok { diff --git a/gateway/internal/features/feature/failover_test.go b/gateway/internal/features/kong/feature/failover_test.go similarity index 98% rename from gateway/internal/features/feature/failover_test.go rename to gateway/internal/features/kong/feature/failover_test.go index f2d72ea10..a20e8259f 100644 --- a/gateway/internal/features/feature/failover_test.go +++ b/gateway/internal/features/kong/feature/failover_test.go @@ -14,7 +14,7 @@ import ( "github.com/telekom/controlplane/common/pkg/util/contextutil" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,13 +25,13 @@ var _ = Describe("FailoverFeature", func() { var ( ctx context.Context f *feature.FailoverFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = contextutil.WithEnv(context.Background(), "test-env") f = feature.InstanceFailoverFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/header_transformation.go b/gateway/internal/features/kong/feature/header_transformation.go similarity index 89% rename from gateway/internal/features/feature/header_transformation.go rename to gateway/internal/features/kong/feature/header_transformation.go index 2372bf962..0afbf52ea 100644 --- a/gateway/internal/features/feature/header_transformation.go +++ b/gateway/internal/features/kong/feature/header_transformation.go @@ -11,7 +11,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &HeaderTransformationFeature{} +var _ features.KongFeature = &HeaderTransformationFeature{} type HeaderTransformationFeature struct { priority int @@ -29,7 +29,7 @@ func (f *HeaderTransformationFeature) Priority() int { return f.priority } -func (f *HeaderTransformationFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *HeaderTransformationFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -41,7 +41,7 @@ func (f *HeaderTransformationFeature) IsUsed(ctx context.Context, builder featur return isPrimaryRoute && hasTransformation } -func (f *HeaderTransformationFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *HeaderTransformationFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/header_transformation_test.go b/gateway/internal/features/kong/feature/header_transformation_test.go similarity index 96% rename from gateway/internal/features/feature/header_transformation_test.go rename to gateway/internal/features/kong/feature/header_transformation_test.go index f1feb12ac..d28834746 100644 --- a/gateway/internal/features/feature/header_transformation_test.go +++ b/gateway/internal/features/kong/feature/header_transformation_test.go @@ -12,7 +12,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) @@ -22,13 +22,13 @@ var _ = Describe("HeaderTransformationFeature", func() { var ( ctx context.Context f *feature.HeaderTransformationFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceHeaderTransformationFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/iprestriction.go b/gateway/internal/features/kong/feature/iprestriction.go similarity index 90% rename from gateway/internal/features/feature/iprestriction.go rename to gateway/internal/features/kong/feature/iprestriction.go index a53a2dd5e..5f408cd7b 100644 --- a/gateway/internal/features/feature/iprestriction.go +++ b/gateway/internal/features/kong/feature/iprestriction.go @@ -11,7 +11,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &IpRestrictionFeature{} +var _ features.KongFeature = &IpRestrictionFeature{} // This feature implements IP restriction for consumers. // At the moment, it is only used for the consumer. @@ -32,7 +32,7 @@ func (f *IpRestrictionFeature) Priority() int { return f.priority } -func (f *IpRestrictionFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *IpRestrictionFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { consumer, ok := builder.GetConsumer() if !ok { return false @@ -41,7 +41,7 @@ func (f *IpRestrictionFeature) IsUsed(ctx context.Context, builder features.Feat return consumer.HasIpRestriction() } -func (f *IpRestrictionFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *IpRestrictionFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { consumer, ok := builder.GetConsumer() if !ok { return features.ErrNoConsumer diff --git a/gateway/internal/features/feature/iprestriction_test.go b/gateway/internal/features/kong/feature/iprestriction_test.go similarity index 96% rename from gateway/internal/features/feature/iprestriction_test.go rename to gateway/internal/features/kong/feature/iprestriction_test.go index c149b0370..dda010bb2 100644 --- a/gateway/internal/features/feature/iprestriction_test.go +++ b/gateway/internal/features/kong/feature/iprestriction_test.go @@ -12,7 +12,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" ) @@ -22,13 +22,13 @@ var _ = Describe("IpRestrictionFeature", func() { var ( ctx context.Context f *feature.IpRestrictionFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceIpRestrictionFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/last_mile_security.go b/gateway/internal/features/kong/feature/last_mile_security.go similarity index 95% rename from gateway/internal/features/feature/last_mile_security.go rename to gateway/internal/features/kong/feature/last_mile_security.go index a199784a7..8a725cc55 100644 --- a/gateway/internal/features/feature/last_mile_security.go +++ b/gateway/internal/features/kong/feature/last_mile_security.go @@ -17,7 +17,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &LastMileSecurityFeature{} +var _ features.KongFeature = &LastMileSecurityFeature{} type LastMileSecurityFeature struct { priority int @@ -35,7 +35,7 @@ func (f *LastMileSecurityFeature) Priority() int { return f.priority } -func (f *LastMileSecurityFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *LastMileSecurityFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -44,7 +44,7 @@ func (f *LastMileSecurityFeature) IsUsed(ctx context.Context, builder features.F return !route.Spec.PassThrough && noFailover } -func (f *LastMileSecurityFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *LastMileSecurityFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/last_mile_security_test.go b/gateway/internal/features/kong/feature/last_mile_security_test.go similarity index 98% rename from gateway/internal/features/feature/last_mile_security_test.go rename to gateway/internal/features/kong/feature/last_mile_security_test.go index 3adef8232..c2b627bb4 100644 --- a/gateway/internal/features/feature/last_mile_security_test.go +++ b/gateway/internal/features/kong/feature/last_mile_security_test.go @@ -14,7 +14,7 @@ import ( "github.com/telekom/controlplane/common/pkg/util/contextutil" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" @@ -25,13 +25,13 @@ var _ = Describe("LastMileSecurityFeature", func() { var ( ctx context.Context f *feature.LastMileSecurityFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = contextutil.WithEnv(context.Background(), "test-env") f = feature.InstanceLastMileSecurityFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/load_balancing.go b/gateway/internal/features/kong/feature/load_balancing.go similarity index 95% rename from gateway/internal/features/feature/load_balancing.go rename to gateway/internal/features/kong/feature/load_balancing.go index dea1541b9..c525f2d4e 100644 --- a/gateway/internal/features/feature/load_balancing.go +++ b/gateway/internal/features/kong/feature/load_balancing.go @@ -14,7 +14,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &LoadBalancingFeature{} +var _ features.KongFeature = &LoadBalancingFeature{} type LoadBalancingFeature struct { priority int @@ -32,7 +32,7 @@ func (f *LoadBalancingFeature) Priority() int { return f.priority } -func (f *LoadBalancingFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *LoadBalancingFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -40,7 +40,7 @@ func (f *LoadBalancingFeature) IsUsed(ctx context.Context, builder features.Feat return len(route.Spec.Backend.Upstreams) > 1 } -func (f *LoadBalancingFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *LoadBalancingFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/load_balancing_test.go b/gateway/internal/features/kong/feature/load_balancing_test.go similarity index 97% rename from gateway/internal/features/feature/load_balancing_test.go rename to gateway/internal/features/kong/feature/load_balancing_test.go index b9668c9ca..e1f05594e 100644 --- a/gateway/internal/features/feature/load_balancing_test.go +++ b/gateway/internal/features/kong/feature/load_balancing_test.go @@ -13,7 +13,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" @@ -24,13 +24,13 @@ var _ = Describe("LoadBalancingFeature", func() { var ( ctx context.Context f *feature.LoadBalancingFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceLoadBalancingFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/passthrough.go b/gateway/internal/features/kong/feature/passthrough.go similarity index 87% rename from gateway/internal/features/feature/passthrough.go rename to gateway/internal/features/kong/feature/passthrough.go index 4d1fb212e..8bada523f 100644 --- a/gateway/internal/features/feature/passthrough.go +++ b/gateway/internal/features/kong/feature/passthrough.go @@ -11,7 +11,7 @@ import ( "github.com/telekom/controlplane/gateway/internal/features" ) -var _ features.Feature = &PassThroughFeature{} +var _ features.KongFeature = &PassThroughFeature{} type PassThroughFeature struct { priority int @@ -29,7 +29,7 @@ func (f *PassThroughFeature) Priority() int { return f.priority } -func (f *PassThroughFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *PassThroughFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { route, ok := builder.GetRoute() if !ok { return false @@ -37,7 +37,7 @@ func (f *PassThroughFeature) IsUsed(ctx context.Context, builder features.Featur return len(route.Spec.Backend.Upstreams) > 0 && route.Spec.PassThrough } -func (f *PassThroughFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *PassThroughFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/passthrough_test.go b/gateway/internal/features/kong/feature/passthrough_test.go similarity index 95% rename from gateway/internal/features/feature/passthrough_test.go rename to gateway/internal/features/kong/feature/passthrough_test.go index c6a7e031f..1c49e3072 100644 --- a/gateway/internal/features/feature/passthrough_test.go +++ b/gateway/internal/features/kong/feature/passthrough_test.go @@ -13,7 +13,7 @@ import ( gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client" ) @@ -23,13 +23,13 @@ var _ = Describe("PassThroughFeature", func() { var ( ctx context.Context f *feature.PassThroughFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstancePassThroughFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/priority_test.go b/gateway/internal/features/kong/feature/priority_test.go similarity index 79% rename from gateway/internal/features/feature/priority_test.go rename to gateway/internal/features/kong/feature/priority_test.go index d7b3c4fae..206b2069b 100644 --- a/gateway/internal/features/feature/priority_test.go +++ b/gateway/internal/features/kong/feature/priority_test.go @@ -11,43 +11,43 @@ import ( . "github.com/onsi/gomega" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" ) var _ = Describe("Feature Priority Ordering", func() { DescribeTable("assigns correct absolute priority values to all features", - func(f features.Feature, expectedPriority int) { + func(f features.KongFeature, expectedPriority int) { Expect(f.Priority()).To(Equal(expectedPriority)) }, Entry("PassThrough has priority 0", - features.Feature(feature.InstancePassThroughFeature), 0), + features.KongFeature(feature.InstancePassThroughFeature), 0), Entry("HeaderTransformation has priority 0", - features.Feature(feature.InstanceHeaderTransformationFeature), 0), + features.KongFeature(feature.InstanceHeaderTransformationFeature), 0), Entry("ExternalIDP has priority 9", - features.Feature(feature.InstanceExternalIDPFeature), 9), + features.KongFeature(feature.InstanceExternalIDPFeature), 9), Entry("AccessControl has priority 10", - features.Feature(feature.InstanceAccessControlFeature), 10), + features.KongFeature(feature.InstanceAccessControlFeature), 10), Entry("CustomScopes has priority 10", - features.Feature(feature.InstanceCustomScopesFeature), 10), + features.KongFeature(feature.InstanceCustomScopesFeature), 10), Entry("Claims has priority 10", - features.Feature(feature.InstanceClaimsFeature), 10), + features.KongFeature(feature.InstanceClaimsFeature), 10), Entry("RateLimit has priority 10", - features.Feature(feature.InstanceRateLimitFeature), 10), + features.KongFeature(feature.InstanceRateLimitFeature), 10), Entry("BasicAuth has priority 10", - features.Feature(feature.InstanceBasicAuthFeature), 10), + features.KongFeature(feature.InstanceBasicAuthFeature), 10), Entry("IpRestriction has priority 10", - features.Feature(feature.InstanceIpRestrictionFeature), 10), + features.KongFeature(feature.InstanceIpRestrictionFeature), 10), Entry("LastMileSecurity has priority 100", - features.Feature(feature.InstanceLastMileSecurityFeature), 100), + features.KongFeature(feature.InstanceLastMileSecurityFeature), 100), Entry("DynamicUpstream has priority 101", - features.Feature(feature.InstanceDynamicUpstreamFeature), 101), + features.KongFeature(feature.InstanceDynamicUpstreamFeature), 101), Entry("LoadBalancing has priority 102", - features.Feature(feature.InstanceLoadBalancingFeature), 102), + features.KongFeature(feature.InstanceLoadBalancingFeature), 102), Entry("Failover has priority 109", - features.Feature(feature.InstanceFailoverFeature), 109), + features.KongFeature(feature.InstanceFailoverFeature), 109), Entry("CircuitBreaker has priority 110", - features.Feature(feature.InstanceCircuitBreakerFeature), 110), + features.KongFeature(feature.InstanceCircuitBreakerFeature), 110), ) It("ensures ExternalIDP runs before CustomScopes", func() { @@ -81,7 +81,7 @@ var _ = Describe("Feature Priority Ordering", func() { }) It("sorts features correctly when using sortFeatures-like logic", func() { - allFeatures := []features.Feature{ + allFeatures := []features.KongFeature{ feature.InstanceCircuitBreakerFeature, feature.InstanceFailoverFeature, feature.InstanceLoadBalancingFeature, diff --git a/gateway/internal/features/feature/ratelimit.go b/gateway/internal/features/kong/feature/ratelimit.go similarity index 97% rename from gateway/internal/features/feature/ratelimit.go rename to gateway/internal/features/kong/feature/ratelimit.go index ebaab5211..ed9010f16 100644 --- a/gateway/internal/features/feature/ratelimit.go +++ b/gateway/internal/features/kong/feature/ratelimit.go @@ -16,7 +16,7 @@ import ( secretManagerApi "github.com/telekom/controlplane/secret-manager/api" ) -var _ features.Feature = &RateLimitFeature{} +var _ features.KongFeature = &RateLimitFeature{} // RateLimitFeature takes precedence over CustomScopesFeature type RateLimitFeature struct { @@ -35,7 +35,7 @@ func (f *RateLimitFeature) Priority() int { return f.priority } -func (f *RateLimitFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (f *RateLimitFeature) IsUsed(ctx context.Context, builder features.KongFeatureBuilder) bool { logger := logr.FromContextOrDiscard(ctx) route, ok := builder.GetRoute() if !ok { @@ -61,7 +61,7 @@ func (f *RateLimitFeature) IsUsed(ctx context.Context, builder features.Features return HasRateLimit(route) || anyConsumerHasRateLimiting } -func (f *RateLimitFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) (err error) { +func (f *RateLimitFeature) Apply(ctx context.Context, builder features.KongFeatureBuilder) (err error) { route, ok := builder.GetRoute() if !ok { return features.ErrNoRoute diff --git a/gateway/internal/features/feature/ratelimit_test.go b/gateway/internal/features/kong/feature/ratelimit_test.go similarity index 99% rename from gateway/internal/features/feature/ratelimit_test.go rename to gateway/internal/features/kong/feature/ratelimit_test.go index b6312a93c..66ecbe589 100644 --- a/gateway/internal/features/feature/ratelimit_test.go +++ b/gateway/internal/features/kong/feature/ratelimit_test.go @@ -14,7 +14,7 @@ import ( "github.com/telekom/controlplane/common/pkg/types" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" secretManagerApi "github.com/telekom/controlplane/secret-manager/api" @@ -26,13 +26,13 @@ var _ = Describe("RateLimitFeature", func() { var ( ctx context.Context f *feature.RateLimitFeature - builder *featmock.MockFeaturesBuilder + builder *featmock.MockKongFeatureBuilder ) BeforeEach(func() { ctx = context.Background() f = feature.InstanceRateLimitFeature - builder = featmock.NewMockFeaturesBuilder(GinkgoT()) + builder = featmock.NewMockKongFeatureBuilder(GinkgoT()) }) Describe("Name()", func() { diff --git a/gateway/internal/features/feature/suite_test.go b/gateway/internal/features/kong/feature/suite_test.go similarity index 100% rename from gateway/internal/features/feature/suite_test.go rename to gateway/internal/features/kong/feature/suite_test.go diff --git a/gateway/internal/features/feature/util.go b/gateway/internal/features/kong/feature/util.go similarity index 100% rename from gateway/internal/features/feature/util.go rename to gateway/internal/features/kong/feature/util.go diff --git a/gateway/internal/features/kong/suite_test.go b/gateway/internal/features/kong/suite_test.go new file mode 100644 index 000000000..94cfae318 --- /dev/null +++ b/gateway/internal/features/kong/suite_test.go @@ -0,0 +1,17 @@ +// Copyright 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package kong_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestKong(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Kong Builder Suite") +} diff --git a/gateway/internal/features/mock/mock_Feature.go b/gateway/internal/features/mock/mock_Feature.go index 14712ed2f..21cef8f91 100644 --- a/gateway/internal/features/mock/mock_Feature.go +++ b/gateway/internal/features/mock/mock_Feature.go @@ -1,4 +1,4 @@ -// Copyright 2026 Deutsche Telekom IT GmbH +// SPDX-FileCopyrightText: 2025 Deutsche Telekom IT GmbH // // SPDX-License-Identifier: Apache-2.0 @@ -16,20 +16,20 @@ import ( ) // MockFeature is an autogenerated mock type for the Feature type -type MockFeature struct { +type MockFeature[T features.FeatureBuilder] struct { mock.Mock } -type MockFeature_Expecter struct { +type MockFeature_Expecter[T features.FeatureBuilder] struct { mock *mock.Mock } -func (_m *MockFeature) EXPECT() *MockFeature_Expecter { - return &MockFeature_Expecter{mock: &_m.Mock} +func (_m *MockFeature[T]) EXPECT() *MockFeature_Expecter[T] { + return &MockFeature_Expecter[T]{mock: &_m.Mock} } // Apply provides a mock function with given fields: ctx, builder -func (_m *MockFeature) Apply(ctx context.Context, builder features.FeaturesBuilder) error { +func (_m *MockFeature[T]) Apply(ctx context.Context, builder T) error { ret := _m.Called(ctx, builder) if len(ret) == 0 { @@ -37,7 +37,7 @@ func (_m *MockFeature) Apply(ctx context.Context, builder features.FeaturesBuild } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, features.FeaturesBuilder) error); ok { + if rf, ok := ret.Get(0).(func(context.Context, T) error); ok { r0 = rf(ctx, builder) } else { r0 = ret.Error(0) @@ -47,36 +47,36 @@ func (_m *MockFeature) Apply(ctx context.Context, builder features.FeaturesBuild } // MockFeature_Apply_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Apply' -type MockFeature_Apply_Call struct { +type MockFeature_Apply_Call[T features.FeatureBuilder] struct { *mock.Call } // Apply is a helper method to define mock.On call // - ctx context.Context -// - builder features.FeaturesBuilder -func (_e *MockFeature_Expecter) Apply(ctx interface{}, builder interface{}) *MockFeature_Apply_Call { - return &MockFeature_Apply_Call{Call: _e.mock.On("Apply", ctx, builder)} +// - builder T +func (_e *MockFeature_Expecter[T]) Apply(ctx interface{}, builder interface{}) *MockFeature_Apply_Call[T] { + return &MockFeature_Apply_Call[T]{Call: _e.mock.On("Apply", ctx, builder)} } -func (_c *MockFeature_Apply_Call) Run(run func(ctx context.Context, builder features.FeaturesBuilder)) *MockFeature_Apply_Call { +func (_c *MockFeature_Apply_Call[T]) Run(run func(ctx context.Context, builder T)) *MockFeature_Apply_Call[T] { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(features.FeaturesBuilder)) + run(args[0].(context.Context), args[1].(T)) }) return _c } -func (_c *MockFeature_Apply_Call) Return(_a0 error) *MockFeature_Apply_Call { +func (_c *MockFeature_Apply_Call[T]) Return(_a0 error) *MockFeature_Apply_Call[T] { _c.Call.Return(_a0) return _c } -func (_c *MockFeature_Apply_Call) RunAndReturn(run func(context.Context, features.FeaturesBuilder) error) *MockFeature_Apply_Call { +func (_c *MockFeature_Apply_Call[T]) RunAndReturn(run func(context.Context, T) error) *MockFeature_Apply_Call[T] { _c.Call.Return(run) return _c } // IsUsed provides a mock function with given fields: ctx, builder -func (_m *MockFeature) IsUsed(ctx context.Context, builder features.FeaturesBuilder) bool { +func (_m *MockFeature[T]) IsUsed(ctx context.Context, builder T) bool { ret := _m.Called(ctx, builder) if len(ret) == 0 { @@ -84,7 +84,7 @@ func (_m *MockFeature) IsUsed(ctx context.Context, builder features.FeaturesBuil } var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, features.FeaturesBuilder) bool); ok { + if rf, ok := ret.Get(0).(func(context.Context, T) bool); ok { r0 = rf(ctx, builder) } else { r0 = ret.Get(0).(bool) @@ -94,36 +94,36 @@ func (_m *MockFeature) IsUsed(ctx context.Context, builder features.FeaturesBuil } // MockFeature_IsUsed_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsUsed' -type MockFeature_IsUsed_Call struct { +type MockFeature_IsUsed_Call[T features.FeatureBuilder] struct { *mock.Call } // IsUsed is a helper method to define mock.On call // - ctx context.Context -// - builder features.FeaturesBuilder -func (_e *MockFeature_Expecter) IsUsed(ctx interface{}, builder interface{}) *MockFeature_IsUsed_Call { - return &MockFeature_IsUsed_Call{Call: _e.mock.On("IsUsed", ctx, builder)} +// - builder T +func (_e *MockFeature_Expecter[T]) IsUsed(ctx interface{}, builder interface{}) *MockFeature_IsUsed_Call[T] { + return &MockFeature_IsUsed_Call[T]{Call: _e.mock.On("IsUsed", ctx, builder)} } -func (_c *MockFeature_IsUsed_Call) Run(run func(ctx context.Context, builder features.FeaturesBuilder)) *MockFeature_IsUsed_Call { +func (_c *MockFeature_IsUsed_Call[T]) Run(run func(ctx context.Context, builder T)) *MockFeature_IsUsed_Call[T] { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(features.FeaturesBuilder)) + run(args[0].(context.Context), args[1].(T)) }) return _c } -func (_c *MockFeature_IsUsed_Call) Return(_a0 bool) *MockFeature_IsUsed_Call { +func (_c *MockFeature_IsUsed_Call[T]) Return(_a0 bool) *MockFeature_IsUsed_Call[T] { _c.Call.Return(_a0) return _c } -func (_c *MockFeature_IsUsed_Call) RunAndReturn(run func(context.Context, features.FeaturesBuilder) bool) *MockFeature_IsUsed_Call { +func (_c *MockFeature_IsUsed_Call[T]) RunAndReturn(run func(context.Context, T) bool) *MockFeature_IsUsed_Call[T] { _c.Call.Return(run) return _c } // Name provides a mock function with no fields -func (_m *MockFeature) Name() v1.FeatureType { +func (_m *MockFeature[T]) Name() v1.FeatureType { ret := _m.Called() if len(ret) == 0 { @@ -141,34 +141,34 @@ func (_m *MockFeature) Name() v1.FeatureType { } // MockFeature_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' -type MockFeature_Name_Call struct { +type MockFeature_Name_Call[T features.FeatureBuilder] struct { *mock.Call } // Name is a helper method to define mock.On call -func (_e *MockFeature_Expecter) Name() *MockFeature_Name_Call { - return &MockFeature_Name_Call{Call: _e.mock.On("Name")} +func (_e *MockFeature_Expecter[T]) Name() *MockFeature_Name_Call[T] { + return &MockFeature_Name_Call[T]{Call: _e.mock.On("Name")} } -func (_c *MockFeature_Name_Call) Run(run func()) *MockFeature_Name_Call { +func (_c *MockFeature_Name_Call[T]) Run(run func()) *MockFeature_Name_Call[T] { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockFeature_Name_Call) Return(_a0 v1.FeatureType) *MockFeature_Name_Call { +func (_c *MockFeature_Name_Call[T]) Return(_a0 v1.FeatureType) *MockFeature_Name_Call[T] { _c.Call.Return(_a0) return _c } -func (_c *MockFeature_Name_Call) RunAndReturn(run func() v1.FeatureType) *MockFeature_Name_Call { +func (_c *MockFeature_Name_Call[T]) RunAndReturn(run func() v1.FeatureType) *MockFeature_Name_Call[T] { _c.Call.Return(run) return _c } // Priority provides a mock function with no fields -func (_m *MockFeature) Priority() int { +func (_m *MockFeature[T]) Priority() int { ret := _m.Called() if len(ret) == 0 { @@ -186,39 +186,39 @@ func (_m *MockFeature) Priority() int { } // MockFeature_Priority_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Priority' -type MockFeature_Priority_Call struct { +type MockFeature_Priority_Call[T features.FeatureBuilder] struct { *mock.Call } // Priority is a helper method to define mock.On call -func (_e *MockFeature_Expecter) Priority() *MockFeature_Priority_Call { - return &MockFeature_Priority_Call{Call: _e.mock.On("Priority")} +func (_e *MockFeature_Expecter[T]) Priority() *MockFeature_Priority_Call[T] { + return &MockFeature_Priority_Call[T]{Call: _e.mock.On("Priority")} } -func (_c *MockFeature_Priority_Call) Run(run func()) *MockFeature_Priority_Call { +func (_c *MockFeature_Priority_Call[T]) Run(run func()) *MockFeature_Priority_Call[T] { _c.Call.Run(func(args mock.Arguments) { run() }) return _c } -func (_c *MockFeature_Priority_Call) Return(_a0 int) *MockFeature_Priority_Call { +func (_c *MockFeature_Priority_Call[T]) Return(_a0 int) *MockFeature_Priority_Call[T] { _c.Call.Return(_a0) return _c } -func (_c *MockFeature_Priority_Call) RunAndReturn(run func() int) *MockFeature_Priority_Call { +func (_c *MockFeature_Priority_Call[T]) RunAndReturn(run func() int) *MockFeature_Priority_Call[T] { _c.Call.Return(run) return _c } // NewMockFeature creates a new instance of MockFeature. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. -func NewMockFeature(t interface { +func NewMockFeature[T features.FeatureBuilder](t interface { mock.TestingT Cleanup(func()) -}) *MockFeature { - mock := &MockFeature{} +}) *MockFeature[T] { + mock := &MockFeature[T]{} mock.Mock.Test(t) t.Cleanup(func() { mock.AssertExpectations(t) }) diff --git a/gateway/internal/features/mock/mock_FeaturesBuilder.go b/gateway/internal/features/mock/mock_FeaturesBuilder.go deleted file mode 100644 index 113d378ff..000000000 --- a/gateway/internal/features/mock/mock_FeaturesBuilder.go +++ /dev/null @@ -1,884 +0,0 @@ -// Copyright 2026 Deutsche Telekom IT GmbH -// -// SPDX-License-Identifier: Apache-2.0 - -// Code generated by mockery v2.53.6. DO NOT EDIT. - -package mock - -import ( - context "context" - - client "github.com/telekom/controlplane/gateway/pkg/kong/client" - - features "github.com/telekom/controlplane/gateway/internal/features" - - mock "github.com/stretchr/testify/mock" - - plugin "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" - - v1 "github.com/telekom/controlplane/gateway/api/v1" -) - -// MockFeaturesBuilder is an autogenerated mock type for the FeaturesBuilder type -type MockFeaturesBuilder struct { - mock.Mock -} - -type MockFeaturesBuilder_Expecter struct { - mock *mock.Mock -} - -func (_m *MockFeaturesBuilder) EXPECT() *MockFeaturesBuilder_Expecter { - return &MockFeaturesBuilder_Expecter{mock: &_m.Mock} -} - -// AclPlugin provides a mock function with no fields -func (_m *MockFeaturesBuilder) AclPlugin() *plugin.AclPlugin { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for AclPlugin") - } - - var r0 *plugin.AclPlugin - if rf, ok := ret.Get(0).(func() *plugin.AclPlugin); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.AclPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_AclPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AclPlugin' -type MockFeaturesBuilder_AclPlugin_Call struct { - *mock.Call -} - -// AclPlugin is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) AclPlugin() *MockFeaturesBuilder_AclPlugin_Call { - return &MockFeaturesBuilder_AclPlugin_Call{Call: _e.mock.On("AclPlugin")} -} - -func (_c *MockFeaturesBuilder_AclPlugin_Call) Run(run func()) *MockFeaturesBuilder_AclPlugin_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_AclPlugin_Call) Return(_a0 *plugin.AclPlugin) *MockFeaturesBuilder_AclPlugin_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_AclPlugin_Call) RunAndReturn(run func() *plugin.AclPlugin) *MockFeaturesBuilder_AclPlugin_Call { - _c.Call.Return(run) - return _c -} - -// AddAllowedConsumers provides a mock function with given fields: _a0 -func (_m *MockFeaturesBuilder) AddAllowedConsumers(_a0 ...*v1.ConsumeRoute) { - _va := make([]interface{}, len(_a0)) - for _i := range _a0 { - _va[_i] = _a0[_i] - } - var _ca []interface{} - _ca = append(_ca, _va...) - _m.Called(_ca...) -} - -// MockFeaturesBuilder_AddAllowedConsumers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAllowedConsumers' -type MockFeaturesBuilder_AddAllowedConsumers_Call struct { - *mock.Call -} - -// AddAllowedConsumers is a helper method to define mock.On call -// - _a0 ...*v1.ConsumeRoute -func (_e *MockFeaturesBuilder_Expecter) AddAllowedConsumers(_a0 ...interface{}) *MockFeaturesBuilder_AddAllowedConsumers_Call { - return &MockFeaturesBuilder_AddAllowedConsumers_Call{Call: _e.mock.On("AddAllowedConsumers", - append([]interface{}{}, _a0...)...)} -} - -func (_c *MockFeaturesBuilder_AddAllowedConsumers_Call) Run(run func(_a0 ...*v1.ConsumeRoute)) *MockFeaturesBuilder_AddAllowedConsumers_Call { - _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]*v1.ConsumeRoute, len(args)-0) - for i, a := range args[0:] { - if a != nil { - variadicArgs[i] = a.(*v1.ConsumeRoute) - } - } - run(variadicArgs...) - }) - return _c -} - -func (_c *MockFeaturesBuilder_AddAllowedConsumers_Call) Return() *MockFeaturesBuilder_AddAllowedConsumers_Call { - _c.Call.Return() - return _c -} - -func (_c *MockFeaturesBuilder_AddAllowedConsumers_Call) RunAndReturn(run func(...*v1.ConsumeRoute)) *MockFeaturesBuilder_AddAllowedConsumers_Call { - _c.Run(run) - return _c -} - -// Build provides a mock function with given fields: _a0 -func (_m *MockFeaturesBuilder) Build(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for Build") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockFeaturesBuilder_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build' -type MockFeaturesBuilder_Build_Call struct { - *mock.Call -} - -// Build is a helper method to define mock.On call -// - _a0 context.Context -func (_e *MockFeaturesBuilder_Expecter) Build(_a0 interface{}) *MockFeaturesBuilder_Build_Call { - return &MockFeaturesBuilder_Build_Call{Call: _e.mock.On("Build", _a0)} -} - -func (_c *MockFeaturesBuilder_Build_Call) Run(run func(_a0 context.Context)) *MockFeaturesBuilder_Build_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockFeaturesBuilder_Build_Call) Return(_a0 error) *MockFeaturesBuilder_Build_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_Build_Call) RunAndReturn(run func(context.Context) error) *MockFeaturesBuilder_Build_Call { - _c.Call.Return(run) - return _c -} - -// BuildForConsumer provides a mock function with given fields: _a0 -func (_m *MockFeaturesBuilder) BuildForConsumer(_a0 context.Context) error { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for BuildForConsumer") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(_a0) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// MockFeaturesBuilder_BuildForConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BuildForConsumer' -type MockFeaturesBuilder_BuildForConsumer_Call struct { - *mock.Call -} - -// BuildForConsumer is a helper method to define mock.On call -// - _a0 context.Context -func (_e *MockFeaturesBuilder_Expecter) BuildForConsumer(_a0 interface{}) *MockFeaturesBuilder_BuildForConsumer_Call { - return &MockFeaturesBuilder_BuildForConsumer_Call{Call: _e.mock.On("BuildForConsumer", _a0)} -} - -func (_c *MockFeaturesBuilder_BuildForConsumer_Call) Run(run func(_a0 context.Context)) *MockFeaturesBuilder_BuildForConsumer_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *MockFeaturesBuilder_BuildForConsumer_Call) Return(_a0 error) *MockFeaturesBuilder_BuildForConsumer_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_BuildForConsumer_Call) RunAndReturn(run func(context.Context) error) *MockFeaturesBuilder_BuildForConsumer_Call { - _c.Call.Return(run) - return _c -} - -// EnableFeature provides a mock function with given fields: f -func (_m *MockFeaturesBuilder) EnableFeature(f features.Feature) { - _m.Called(f) -} - -// MockFeaturesBuilder_EnableFeature_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableFeature' -type MockFeaturesBuilder_EnableFeature_Call struct { - *mock.Call -} - -// EnableFeature is a helper method to define mock.On call -// - f features.Feature -func (_e *MockFeaturesBuilder_Expecter) EnableFeature(f interface{}) *MockFeaturesBuilder_EnableFeature_Call { - return &MockFeaturesBuilder_EnableFeature_Call{Call: _e.mock.On("EnableFeature", f)} -} - -func (_c *MockFeaturesBuilder_EnableFeature_Call) Run(run func(f features.Feature)) *MockFeaturesBuilder_EnableFeature_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(features.Feature)) - }) - return _c -} - -func (_c *MockFeaturesBuilder_EnableFeature_Call) Return() *MockFeaturesBuilder_EnableFeature_Call { - _c.Call.Return() - return _c -} - -func (_c *MockFeaturesBuilder_EnableFeature_Call) RunAndReturn(run func(features.Feature)) *MockFeaturesBuilder_EnableFeature_Call { - _c.Run(run) - return _c -} - -// GetAllowedConsumers provides a mock function with no fields -func (_m *MockFeaturesBuilder) GetAllowedConsumers() []*v1.ConsumeRoute { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetAllowedConsumers") - } - - var r0 []*v1.ConsumeRoute - if rf, ok := ret.Get(0).(func() []*v1.ConsumeRoute); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]*v1.ConsumeRoute) - } - } - - return r0 -} - -// MockFeaturesBuilder_GetAllowedConsumers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllowedConsumers' -type MockFeaturesBuilder_GetAllowedConsumers_Call struct { - *mock.Call -} - -// GetAllowedConsumers is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) GetAllowedConsumers() *MockFeaturesBuilder_GetAllowedConsumers_Call { - return &MockFeaturesBuilder_GetAllowedConsumers_Call{Call: _e.mock.On("GetAllowedConsumers")} -} - -func (_c *MockFeaturesBuilder_GetAllowedConsumers_Call) Run(run func()) *MockFeaturesBuilder_GetAllowedConsumers_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_GetAllowedConsumers_Call) Return(_a0 []*v1.ConsumeRoute) *MockFeaturesBuilder_GetAllowedConsumers_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_GetAllowedConsumers_Call) RunAndReturn(run func() []*v1.ConsumeRoute) *MockFeaturesBuilder_GetAllowedConsumers_Call { - _c.Call.Return(run) - return _c -} - -// GetConsumer provides a mock function with no fields -func (_m *MockFeaturesBuilder) GetConsumer() (*v1.Consumer, bool) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetConsumer") - } - - var r0 *v1.Consumer - var r1 bool - if rf, ok := ret.Get(0).(func() (*v1.Consumer, bool)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *v1.Consumer); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Consumer) - } - } - - if rf, ok := ret.Get(1).(func() bool); ok { - r1 = rf() - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// MockFeaturesBuilder_GetConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConsumer' -type MockFeaturesBuilder_GetConsumer_Call struct { - *mock.Call -} - -// GetConsumer is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) GetConsumer() *MockFeaturesBuilder_GetConsumer_Call { - return &MockFeaturesBuilder_GetConsumer_Call{Call: _e.mock.On("GetConsumer")} -} - -func (_c *MockFeaturesBuilder_GetConsumer_Call) Run(run func()) *MockFeaturesBuilder_GetConsumer_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_GetConsumer_Call) Return(_a0 *v1.Consumer, _a1 bool) *MockFeaturesBuilder_GetConsumer_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockFeaturesBuilder_GetConsumer_Call) RunAndReturn(run func() (*v1.Consumer, bool)) *MockFeaturesBuilder_GetConsumer_Call { - _c.Call.Return(run) - return _c -} - -// GetGateway provides a mock function with no fields -func (_m *MockFeaturesBuilder) GetGateway() *v1.Gateway { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetGateway") - } - - var r0 *v1.Gateway - if rf, ok := ret.Get(0).(func() *v1.Gateway); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Gateway) - } - } - - return r0 -} - -// MockFeaturesBuilder_GetGateway_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetGateway' -type MockFeaturesBuilder_GetGateway_Call struct { - *mock.Call -} - -// GetGateway is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) GetGateway() *MockFeaturesBuilder_GetGateway_Call { - return &MockFeaturesBuilder_GetGateway_Call{Call: _e.mock.On("GetGateway")} -} - -func (_c *MockFeaturesBuilder_GetGateway_Call) Run(run func()) *MockFeaturesBuilder_GetGateway_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_GetGateway_Call) Return(_a0 *v1.Gateway) *MockFeaturesBuilder_GetGateway_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_GetGateway_Call) RunAndReturn(run func() *v1.Gateway) *MockFeaturesBuilder_GetGateway_Call { - _c.Call.Return(run) - return _c -} - -// GetKongClient provides a mock function with no fields -func (_m *MockFeaturesBuilder) GetKongClient() client.KongClient { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetKongClient") - } - - var r0 client.KongClient - if rf, ok := ret.Get(0).(func() client.KongClient); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(client.KongClient) - } - } - - return r0 -} - -// MockFeaturesBuilder_GetKongClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKongClient' -type MockFeaturesBuilder_GetKongClient_Call struct { - *mock.Call -} - -// GetKongClient is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) GetKongClient() *MockFeaturesBuilder_GetKongClient_Call { - return &MockFeaturesBuilder_GetKongClient_Call{Call: _e.mock.On("GetKongClient")} -} - -func (_c *MockFeaturesBuilder_GetKongClient_Call) Run(run func()) *MockFeaturesBuilder_GetKongClient_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_GetKongClient_Call) Return(_a0 client.KongClient) *MockFeaturesBuilder_GetKongClient_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_GetKongClient_Call) RunAndReturn(run func() client.KongClient) *MockFeaturesBuilder_GetKongClient_Call { - _c.Call.Return(run) - return _c -} - -// GetRoute provides a mock function with no fields -func (_m *MockFeaturesBuilder) GetRoute() (*v1.Route, bool) { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for GetRoute") - } - - var r0 *v1.Route - var r1 bool - if rf, ok := ret.Get(0).(func() (*v1.Route, bool)); ok { - return rf() - } - if rf, ok := ret.Get(0).(func() *v1.Route); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*v1.Route) - } - } - - if rf, ok := ret.Get(1).(func() bool); ok { - r1 = rf() - } else { - r1 = ret.Get(1).(bool) - } - - return r0, r1 -} - -// MockFeaturesBuilder_GetRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoute' -type MockFeaturesBuilder_GetRoute_Call struct { - *mock.Call -} - -// GetRoute is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) GetRoute() *MockFeaturesBuilder_GetRoute_Call { - return &MockFeaturesBuilder_GetRoute_Call{Call: _e.mock.On("GetRoute")} -} - -func (_c *MockFeaturesBuilder_GetRoute_Call) Run(run func()) *MockFeaturesBuilder_GetRoute_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_GetRoute_Call) Return(_a0 *v1.Route, _a1 bool) *MockFeaturesBuilder_GetRoute_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *MockFeaturesBuilder_GetRoute_Call) RunAndReturn(run func() (*v1.Route, bool)) *MockFeaturesBuilder_GetRoute_Call { - _c.Call.Return(run) - return _c -} - -// IpRestrictionPlugin provides a mock function with no fields -func (_m *MockFeaturesBuilder) IpRestrictionPlugin() *plugin.IpRestrictionPlugin { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for IpRestrictionPlugin") - } - - var r0 *plugin.IpRestrictionPlugin - if rf, ok := ret.Get(0).(func() *plugin.IpRestrictionPlugin); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.IpRestrictionPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_IpRestrictionPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IpRestrictionPlugin' -type MockFeaturesBuilder_IpRestrictionPlugin_Call struct { - *mock.Call -} - -// IpRestrictionPlugin is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) IpRestrictionPlugin() *MockFeaturesBuilder_IpRestrictionPlugin_Call { - return &MockFeaturesBuilder_IpRestrictionPlugin_Call{Call: _e.mock.On("IpRestrictionPlugin")} -} - -func (_c *MockFeaturesBuilder_IpRestrictionPlugin_Call) Run(run func()) *MockFeaturesBuilder_IpRestrictionPlugin_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_IpRestrictionPlugin_Call) Return(_a0 *plugin.IpRestrictionPlugin) *MockFeaturesBuilder_IpRestrictionPlugin_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_IpRestrictionPlugin_Call) RunAndReturn(run func() *plugin.IpRestrictionPlugin) *MockFeaturesBuilder_IpRestrictionPlugin_Call { - _c.Call.Return(run) - return _c -} - -// JumperConfig provides a mock function with no fields -func (_m *MockFeaturesBuilder) JumperConfig() *plugin.JumperConfig { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for JumperConfig") - } - - var r0 *plugin.JumperConfig - if rf, ok := ret.Get(0).(func() *plugin.JumperConfig); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.JumperConfig) - } - } - - return r0 -} - -// MockFeaturesBuilder_JumperConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JumperConfig' -type MockFeaturesBuilder_JumperConfig_Call struct { - *mock.Call -} - -// JumperConfig is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) JumperConfig() *MockFeaturesBuilder_JumperConfig_Call { - return &MockFeaturesBuilder_JumperConfig_Call{Call: _e.mock.On("JumperConfig")} -} - -func (_c *MockFeaturesBuilder_JumperConfig_Call) Run(run func()) *MockFeaturesBuilder_JumperConfig_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_JumperConfig_Call) Return(_a0 *plugin.JumperConfig) *MockFeaturesBuilder_JumperConfig_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_JumperConfig_Call) RunAndReturn(run func() *plugin.JumperConfig) *MockFeaturesBuilder_JumperConfig_Call { - _c.Call.Return(run) - return _c -} - -// JwtPlugin provides a mock function with no fields -func (_m *MockFeaturesBuilder) JwtPlugin() *plugin.JwtPlugin { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for JwtPlugin") - } - - var r0 *plugin.JwtPlugin - if rf, ok := ret.Get(0).(func() *plugin.JwtPlugin); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.JwtPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_JwtPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JwtPlugin' -type MockFeaturesBuilder_JwtPlugin_Call struct { - *mock.Call -} - -// JwtPlugin is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) JwtPlugin() *MockFeaturesBuilder_JwtPlugin_Call { - return &MockFeaturesBuilder_JwtPlugin_Call{Call: _e.mock.On("JwtPlugin")} -} - -func (_c *MockFeaturesBuilder_JwtPlugin_Call) Run(run func()) *MockFeaturesBuilder_JwtPlugin_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_JwtPlugin_Call) Return(_a0 *plugin.JwtPlugin) *MockFeaturesBuilder_JwtPlugin_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_JwtPlugin_Call) RunAndReturn(run func() *plugin.JwtPlugin) *MockFeaturesBuilder_JwtPlugin_Call { - _c.Call.Return(run) - return _c -} - -// RateLimitPluginConsumeRoute provides a mock function with given fields: _a0 -func (_m *MockFeaturesBuilder) RateLimitPluginConsumeRoute(_a0 *v1.ConsumeRoute) *plugin.RateLimitPlugin { - ret := _m.Called(_a0) - - if len(ret) == 0 { - panic("no return value specified for RateLimitPluginConsumeRoute") - } - - var r0 *plugin.RateLimitPlugin - if rf, ok := ret.Get(0).(func(*v1.ConsumeRoute) *plugin.RateLimitPlugin); ok { - r0 = rf(_a0) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.RateLimitPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateLimitPluginConsumeRoute' -type MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call struct { - *mock.Call -} - -// RateLimitPluginConsumeRoute is a helper method to define mock.On call -// - _a0 *v1.ConsumeRoute -func (_e *MockFeaturesBuilder_Expecter) RateLimitPluginConsumeRoute(_a0 interface{}) *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call { - return &MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call{Call: _e.mock.On("RateLimitPluginConsumeRoute", _a0)} -} - -func (_c *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call) Run(run func(_a0 *v1.ConsumeRoute)) *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(*v1.ConsumeRoute)) - }) - return _c -} - -func (_c *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call) Return(_a0 *plugin.RateLimitPlugin) *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call) RunAndReturn(run func(*v1.ConsumeRoute) *plugin.RateLimitPlugin) *MockFeaturesBuilder_RateLimitPluginConsumeRoute_Call { - _c.Call.Return(run) - return _c -} - -// RateLimitPluginRoute provides a mock function with no fields -func (_m *MockFeaturesBuilder) RateLimitPluginRoute() *plugin.RateLimitPlugin { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for RateLimitPluginRoute") - } - - var r0 *plugin.RateLimitPlugin - if rf, ok := ret.Get(0).(func() *plugin.RateLimitPlugin); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.RateLimitPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_RateLimitPluginRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateLimitPluginRoute' -type MockFeaturesBuilder_RateLimitPluginRoute_Call struct { - *mock.Call -} - -// RateLimitPluginRoute is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) RateLimitPluginRoute() *MockFeaturesBuilder_RateLimitPluginRoute_Call { - return &MockFeaturesBuilder_RateLimitPluginRoute_Call{Call: _e.mock.On("RateLimitPluginRoute")} -} - -func (_c *MockFeaturesBuilder_RateLimitPluginRoute_Call) Run(run func()) *MockFeaturesBuilder_RateLimitPluginRoute_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_RateLimitPluginRoute_Call) Return(_a0 *plugin.RateLimitPlugin) *MockFeaturesBuilder_RateLimitPluginRoute_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_RateLimitPluginRoute_Call) RunAndReturn(run func() *plugin.RateLimitPlugin) *MockFeaturesBuilder_RateLimitPluginRoute_Call { - _c.Call.Return(run) - return _c -} - -// RequestTransformerPlugin provides a mock function with no fields -func (_m *MockFeaturesBuilder) RequestTransformerPlugin() *plugin.RequestTransformerPlugin { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for RequestTransformerPlugin") - } - - var r0 *plugin.RequestTransformerPlugin - if rf, ok := ret.Get(0).(func() *plugin.RequestTransformerPlugin); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.RequestTransformerPlugin) - } - } - - return r0 -} - -// MockFeaturesBuilder_RequestTransformerPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestTransformerPlugin' -type MockFeaturesBuilder_RequestTransformerPlugin_Call struct { - *mock.Call -} - -// RequestTransformerPlugin is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) RequestTransformerPlugin() *MockFeaturesBuilder_RequestTransformerPlugin_Call { - return &MockFeaturesBuilder_RequestTransformerPlugin_Call{Call: _e.mock.On("RequestTransformerPlugin")} -} - -func (_c *MockFeaturesBuilder_RequestTransformerPlugin_Call) Run(run func()) *MockFeaturesBuilder_RequestTransformerPlugin_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_RequestTransformerPlugin_Call) Return(_a0 *plugin.RequestTransformerPlugin) *MockFeaturesBuilder_RequestTransformerPlugin_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_RequestTransformerPlugin_Call) RunAndReturn(run func() *plugin.RequestTransformerPlugin) *MockFeaturesBuilder_RequestTransformerPlugin_Call { - _c.Call.Return(run) - return _c -} - -// RoutingConfigs provides a mock function with no fields -func (_m *MockFeaturesBuilder) RoutingConfigs() *plugin.RoutingConfigs { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for RoutingConfigs") - } - - var r0 *plugin.RoutingConfigs - if rf, ok := ret.Get(0).(func() *plugin.RoutingConfigs); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*plugin.RoutingConfigs) - } - } - - return r0 -} - -// MockFeaturesBuilder_RoutingConfigs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RoutingConfigs' -type MockFeaturesBuilder_RoutingConfigs_Call struct { - *mock.Call -} - -// RoutingConfigs is a helper method to define mock.On call -func (_e *MockFeaturesBuilder_Expecter) RoutingConfigs() *MockFeaturesBuilder_RoutingConfigs_Call { - return &MockFeaturesBuilder_RoutingConfigs_Call{Call: _e.mock.On("RoutingConfigs")} -} - -func (_c *MockFeaturesBuilder_RoutingConfigs_Call) Run(run func()) *MockFeaturesBuilder_RoutingConfigs_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockFeaturesBuilder_RoutingConfigs_Call) Return(_a0 *plugin.RoutingConfigs) *MockFeaturesBuilder_RoutingConfigs_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockFeaturesBuilder_RoutingConfigs_Call) RunAndReturn(run func() *plugin.RoutingConfigs) *MockFeaturesBuilder_RoutingConfigs_Call { - _c.Call.Return(run) - return _c -} - -// SetUpstream provides a mock function with given fields: _a0 -func (_m *MockFeaturesBuilder) SetUpstream(_a0 client.Upstream) { - _m.Called(_a0) -} - -// MockFeaturesBuilder_SetUpstream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUpstream' -type MockFeaturesBuilder_SetUpstream_Call struct { - *mock.Call -} - -// SetUpstream is a helper method to define mock.On call -// - _a0 client.Upstream -func (_e *MockFeaturesBuilder_Expecter) SetUpstream(_a0 interface{}) *MockFeaturesBuilder_SetUpstream_Call { - return &MockFeaturesBuilder_SetUpstream_Call{Call: _e.mock.On("SetUpstream", _a0)} -} - -func (_c *MockFeaturesBuilder_SetUpstream_Call) Run(run func(_a0 client.Upstream)) *MockFeaturesBuilder_SetUpstream_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(client.Upstream)) - }) - return _c -} - -func (_c *MockFeaturesBuilder_SetUpstream_Call) Return() *MockFeaturesBuilder_SetUpstream_Call { - _c.Call.Return() - return _c -} - -func (_c *MockFeaturesBuilder_SetUpstream_Call) RunAndReturn(run func(client.Upstream)) *MockFeaturesBuilder_SetUpstream_Call { - _c.Run(run) - return _c -} - -// NewMockFeaturesBuilder creates a new instance of MockFeaturesBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewMockFeaturesBuilder(t interface { - mock.TestingT - Cleanup(func()) -}) *MockFeaturesBuilder { - mock := &MockFeaturesBuilder{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/gateway/internal/features/mock/mock_KongFeatureBuilder.go b/gateway/internal/features/mock/mock_KongFeatureBuilder.go new file mode 100644 index 000000000..c651fa873 --- /dev/null +++ b/gateway/internal/features/mock/mock_KongFeatureBuilder.go @@ -0,0 +1,884 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by mockery v2.53.6. DO NOT EDIT. + +package mock + +import ( + context "context" + + client "github.com/telekom/controlplane/gateway/pkg/kong/client" + + features "github.com/telekom/controlplane/gateway/internal/features" + + mock "github.com/stretchr/testify/mock" + + plugin "github.com/telekom/controlplane/gateway/pkg/kong/client/plugin" + + v1 "github.com/telekom/controlplane/gateway/api/v1" +) + +// MockKongFeatureBuilder is an autogenerated mock type for the KongFeatureBuilder type +type MockKongFeatureBuilder struct { + mock.Mock +} + +type MockKongFeatureBuilder_Expecter struct { + mock *mock.Mock +} + +func (_m *MockKongFeatureBuilder) EXPECT() *MockKongFeatureBuilder_Expecter { + return &MockKongFeatureBuilder_Expecter{mock: &_m.Mock} +} + +// AclPlugin provides a mock function with no fields +func (_m *MockKongFeatureBuilder) AclPlugin() *plugin.AclPlugin { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for AclPlugin") + } + + var r0 *plugin.AclPlugin + if rf, ok := ret.Get(0).(func() *plugin.AclPlugin); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.AclPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_AclPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AclPlugin' +type MockKongFeatureBuilder_AclPlugin_Call struct { + *mock.Call +} + +// AclPlugin is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) AclPlugin() *MockKongFeatureBuilder_AclPlugin_Call { + return &MockKongFeatureBuilder_AclPlugin_Call{Call: _e.mock.On("AclPlugin")} +} + +func (_c *MockKongFeatureBuilder_AclPlugin_Call) Run(run func()) *MockKongFeatureBuilder_AclPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_AclPlugin_Call) Return(_a0 *plugin.AclPlugin) *MockKongFeatureBuilder_AclPlugin_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_AclPlugin_Call) RunAndReturn(run func() *plugin.AclPlugin) *MockKongFeatureBuilder_AclPlugin_Call { + _c.Call.Return(run) + return _c +} + +// AddAllowedConsumers provides a mock function with given fields: _a0 +func (_m *MockKongFeatureBuilder) AddAllowedConsumers(_a0 ...*v1.ConsumeRoute) { + _va := make([]interface{}, len(_a0)) + for _i := range _a0 { + _va[_i] = _a0[_i] + } + var _ca []interface{} + _ca = append(_ca, _va...) + _m.Called(_ca...) +} + +// MockKongFeatureBuilder_AddAllowedConsumers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddAllowedConsumers' +type MockKongFeatureBuilder_AddAllowedConsumers_Call struct { + *mock.Call +} + +// AddAllowedConsumers is a helper method to define mock.On call +// - _a0 ...*v1.ConsumeRoute +func (_e *MockKongFeatureBuilder_Expecter) AddAllowedConsumers(_a0 ...interface{}) *MockKongFeatureBuilder_AddAllowedConsumers_Call { + return &MockKongFeatureBuilder_AddAllowedConsumers_Call{Call: _e.mock.On("AddAllowedConsumers", + append([]interface{}{}, _a0...)...)} +} + +func (_c *MockKongFeatureBuilder_AddAllowedConsumers_Call) Run(run func(_a0 ...*v1.ConsumeRoute)) *MockKongFeatureBuilder_AddAllowedConsumers_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]*v1.ConsumeRoute, len(args)-0) + for i, a := range args[0:] { + if a != nil { + variadicArgs[i] = a.(*v1.ConsumeRoute) + } + } + run(variadicArgs...) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_AddAllowedConsumers_Call) Return() *MockKongFeatureBuilder_AddAllowedConsumers_Call { + _c.Call.Return() + return _c +} + +func (_c *MockKongFeatureBuilder_AddAllowedConsumers_Call) RunAndReturn(run func(...*v1.ConsumeRoute)) *MockKongFeatureBuilder_AddAllowedConsumers_Call { + _c.Run(run) + return _c +} + +// Build provides a mock function with given fields: _a0 +func (_m *MockKongFeatureBuilder) Build(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Build") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockKongFeatureBuilder_Build_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Build' +type MockKongFeatureBuilder_Build_Call struct { + *mock.Call +} + +// Build is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockKongFeatureBuilder_Expecter) Build(_a0 interface{}) *MockKongFeatureBuilder_Build_Call { + return &MockKongFeatureBuilder_Build_Call{Call: _e.mock.On("Build", _a0)} +} + +func (_c *MockKongFeatureBuilder_Build_Call) Run(run func(_a0 context.Context)) *MockKongFeatureBuilder_Build_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_Build_Call) Return(_a0 error) *MockKongFeatureBuilder_Build_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_Build_Call) RunAndReturn(run func(context.Context) error) *MockKongFeatureBuilder_Build_Call { + _c.Call.Return(run) + return _c +} + +// BuildForConsumer provides a mock function with given fields: _a0 +func (_m *MockKongFeatureBuilder) BuildForConsumer(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for BuildForConsumer") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// MockKongFeatureBuilder_BuildForConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BuildForConsumer' +type MockKongFeatureBuilder_BuildForConsumer_Call struct { + *mock.Call +} + +// BuildForConsumer is a helper method to define mock.On call +// - _a0 context.Context +func (_e *MockKongFeatureBuilder_Expecter) BuildForConsumer(_a0 interface{}) *MockKongFeatureBuilder_BuildForConsumer_Call { + return &MockKongFeatureBuilder_BuildForConsumer_Call{Call: _e.mock.On("BuildForConsumer", _a0)} +} + +func (_c *MockKongFeatureBuilder_BuildForConsumer_Call) Run(run func(_a0 context.Context)) *MockKongFeatureBuilder_BuildForConsumer_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_BuildForConsumer_Call) Return(_a0 error) *MockKongFeatureBuilder_BuildForConsumer_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_BuildForConsumer_Call) RunAndReturn(run func(context.Context) error) *MockKongFeatureBuilder_BuildForConsumer_Call { + _c.Call.Return(run) + return _c +} + +// EnableFeature provides a mock function with given fields: f +func (_m *MockKongFeatureBuilder) EnableFeature(f features.Feature[features.KongFeatureBuilder]) { + _m.Called(f) +} + +// MockKongFeatureBuilder_EnableFeature_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableFeature' +type MockKongFeatureBuilder_EnableFeature_Call struct { + *mock.Call +} + +// EnableFeature is a helper method to define mock.On call +// - f features.Feature[features.KongFeatureBuilder] +func (_e *MockKongFeatureBuilder_Expecter) EnableFeature(f interface{}) *MockKongFeatureBuilder_EnableFeature_Call { + return &MockKongFeatureBuilder_EnableFeature_Call{Call: _e.mock.On("EnableFeature", f)} +} + +func (_c *MockKongFeatureBuilder_EnableFeature_Call) Run(run func(f features.Feature[features.KongFeatureBuilder])) *MockKongFeatureBuilder_EnableFeature_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(features.Feature[features.KongFeatureBuilder])) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_EnableFeature_Call) Return() *MockKongFeatureBuilder_EnableFeature_Call { + _c.Call.Return() + return _c +} + +func (_c *MockKongFeatureBuilder_EnableFeature_Call) RunAndReturn(run func(features.Feature[features.KongFeatureBuilder])) *MockKongFeatureBuilder_EnableFeature_Call { + _c.Run(run) + return _c +} + +// GetAllowedConsumers provides a mock function with no fields +func (_m *MockKongFeatureBuilder) GetAllowedConsumers() []*v1.ConsumeRoute { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetAllowedConsumers") + } + + var r0 []*v1.ConsumeRoute + if rf, ok := ret.Get(0).(func() []*v1.ConsumeRoute); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*v1.ConsumeRoute) + } + } + + return r0 +} + +// MockKongFeatureBuilder_GetAllowedConsumers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllowedConsumers' +type MockKongFeatureBuilder_GetAllowedConsumers_Call struct { + *mock.Call +} + +// GetAllowedConsumers is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) GetAllowedConsumers() *MockKongFeatureBuilder_GetAllowedConsumers_Call { + return &MockKongFeatureBuilder_GetAllowedConsumers_Call{Call: _e.mock.On("GetAllowedConsumers")} +} + +func (_c *MockKongFeatureBuilder_GetAllowedConsumers_Call) Run(run func()) *MockKongFeatureBuilder_GetAllowedConsumers_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_GetAllowedConsumers_Call) Return(_a0 []*v1.ConsumeRoute) *MockKongFeatureBuilder_GetAllowedConsumers_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_GetAllowedConsumers_Call) RunAndReturn(run func() []*v1.ConsumeRoute) *MockKongFeatureBuilder_GetAllowedConsumers_Call { + _c.Call.Return(run) + return _c +} + +// GetConsumer provides a mock function with no fields +func (_m *MockKongFeatureBuilder) GetConsumer() (*v1.Consumer, bool) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetConsumer") + } + + var r0 *v1.Consumer + var r1 bool + if rf, ok := ret.Get(0).(func() (*v1.Consumer, bool)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *v1.Consumer); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Consumer) + } + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// MockKongFeatureBuilder_GetConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetConsumer' +type MockKongFeatureBuilder_GetConsumer_Call struct { + *mock.Call +} + +// GetConsumer is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) GetConsumer() *MockKongFeatureBuilder_GetConsumer_Call { + return &MockKongFeatureBuilder_GetConsumer_Call{Call: _e.mock.On("GetConsumer")} +} + +func (_c *MockKongFeatureBuilder_GetConsumer_Call) Run(run func()) *MockKongFeatureBuilder_GetConsumer_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_GetConsumer_Call) Return(_a0 *v1.Consumer, _a1 bool) *MockKongFeatureBuilder_GetConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockKongFeatureBuilder_GetConsumer_Call) RunAndReturn(run func() (*v1.Consumer, bool)) *MockKongFeatureBuilder_GetConsumer_Call { + _c.Call.Return(run) + return _c +} + +// GetGateway provides a mock function with no fields +func (_m *MockKongFeatureBuilder) GetGateway() *v1.Gateway { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetGateway") + } + + var r0 *v1.Gateway + if rf, ok := ret.Get(0).(func() *v1.Gateway); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Gateway) + } + } + + return r0 +} + +// MockKongFeatureBuilder_GetGateway_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetGateway' +type MockKongFeatureBuilder_GetGateway_Call struct { + *mock.Call +} + +// GetGateway is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) GetGateway() *MockKongFeatureBuilder_GetGateway_Call { + return &MockKongFeatureBuilder_GetGateway_Call{Call: _e.mock.On("GetGateway")} +} + +func (_c *MockKongFeatureBuilder_GetGateway_Call) Run(run func()) *MockKongFeatureBuilder_GetGateway_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_GetGateway_Call) Return(_a0 *v1.Gateway) *MockKongFeatureBuilder_GetGateway_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_GetGateway_Call) RunAndReturn(run func() *v1.Gateway) *MockKongFeatureBuilder_GetGateway_Call { + _c.Call.Return(run) + return _c +} + +// GetKongClient provides a mock function with no fields +func (_m *MockKongFeatureBuilder) GetKongClient() client.KongClient { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetKongClient") + } + + var r0 client.KongClient + if rf, ok := ret.Get(0).(func() client.KongClient); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(client.KongClient) + } + } + + return r0 +} + +// MockKongFeatureBuilder_GetKongClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetKongClient' +type MockKongFeatureBuilder_GetKongClient_Call struct { + *mock.Call +} + +// GetKongClient is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) GetKongClient() *MockKongFeatureBuilder_GetKongClient_Call { + return &MockKongFeatureBuilder_GetKongClient_Call{Call: _e.mock.On("GetKongClient")} +} + +func (_c *MockKongFeatureBuilder_GetKongClient_Call) Run(run func()) *MockKongFeatureBuilder_GetKongClient_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_GetKongClient_Call) Return(_a0 client.KongClient) *MockKongFeatureBuilder_GetKongClient_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_GetKongClient_Call) RunAndReturn(run func() client.KongClient) *MockKongFeatureBuilder_GetKongClient_Call { + _c.Call.Return(run) + return _c +} + +// GetRoute provides a mock function with no fields +func (_m *MockKongFeatureBuilder) GetRoute() (*v1.Route, bool) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetRoute") + } + + var r0 *v1.Route + var r1 bool + if rf, ok := ret.Get(0).(func() (*v1.Route, bool)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() *v1.Route); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*v1.Route) + } + } + + if rf, ok := ret.Get(1).(func() bool); ok { + r1 = rf() + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// MockKongFeatureBuilder_GetRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRoute' +type MockKongFeatureBuilder_GetRoute_Call struct { + *mock.Call +} + +// GetRoute is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) GetRoute() *MockKongFeatureBuilder_GetRoute_Call { + return &MockKongFeatureBuilder_GetRoute_Call{Call: _e.mock.On("GetRoute")} +} + +func (_c *MockKongFeatureBuilder_GetRoute_Call) Run(run func()) *MockKongFeatureBuilder_GetRoute_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_GetRoute_Call) Return(_a0 *v1.Route, _a1 bool) *MockKongFeatureBuilder_GetRoute_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockKongFeatureBuilder_GetRoute_Call) RunAndReturn(run func() (*v1.Route, bool)) *MockKongFeatureBuilder_GetRoute_Call { + _c.Call.Return(run) + return _c +} + +// IpRestrictionPlugin provides a mock function with no fields +func (_m *MockKongFeatureBuilder) IpRestrictionPlugin() *plugin.IpRestrictionPlugin { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for IpRestrictionPlugin") + } + + var r0 *plugin.IpRestrictionPlugin + if rf, ok := ret.Get(0).(func() *plugin.IpRestrictionPlugin); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.IpRestrictionPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_IpRestrictionPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IpRestrictionPlugin' +type MockKongFeatureBuilder_IpRestrictionPlugin_Call struct { + *mock.Call +} + +// IpRestrictionPlugin is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) IpRestrictionPlugin() *MockKongFeatureBuilder_IpRestrictionPlugin_Call { + return &MockKongFeatureBuilder_IpRestrictionPlugin_Call{Call: _e.mock.On("IpRestrictionPlugin")} +} + +func (_c *MockKongFeatureBuilder_IpRestrictionPlugin_Call) Run(run func()) *MockKongFeatureBuilder_IpRestrictionPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_IpRestrictionPlugin_Call) Return(_a0 *plugin.IpRestrictionPlugin) *MockKongFeatureBuilder_IpRestrictionPlugin_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_IpRestrictionPlugin_Call) RunAndReturn(run func() *plugin.IpRestrictionPlugin) *MockKongFeatureBuilder_IpRestrictionPlugin_Call { + _c.Call.Return(run) + return _c +} + +// JumperConfig provides a mock function with no fields +func (_m *MockKongFeatureBuilder) JumperConfig() *plugin.JumperConfig { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for JumperConfig") + } + + var r0 *plugin.JumperConfig + if rf, ok := ret.Get(0).(func() *plugin.JumperConfig); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.JumperConfig) + } + } + + return r0 +} + +// MockKongFeatureBuilder_JumperConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JumperConfig' +type MockKongFeatureBuilder_JumperConfig_Call struct { + *mock.Call +} + +// JumperConfig is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) JumperConfig() *MockKongFeatureBuilder_JumperConfig_Call { + return &MockKongFeatureBuilder_JumperConfig_Call{Call: _e.mock.On("JumperConfig")} +} + +func (_c *MockKongFeatureBuilder_JumperConfig_Call) Run(run func()) *MockKongFeatureBuilder_JumperConfig_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_JumperConfig_Call) Return(_a0 *plugin.JumperConfig) *MockKongFeatureBuilder_JumperConfig_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_JumperConfig_Call) RunAndReturn(run func() *plugin.JumperConfig) *MockKongFeatureBuilder_JumperConfig_Call { + _c.Call.Return(run) + return _c +} + +// JwtPlugin provides a mock function with no fields +func (_m *MockKongFeatureBuilder) JwtPlugin() *plugin.JwtPlugin { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for JwtPlugin") + } + + var r0 *plugin.JwtPlugin + if rf, ok := ret.Get(0).(func() *plugin.JwtPlugin); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.JwtPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_JwtPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'JwtPlugin' +type MockKongFeatureBuilder_JwtPlugin_Call struct { + *mock.Call +} + +// JwtPlugin is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) JwtPlugin() *MockKongFeatureBuilder_JwtPlugin_Call { + return &MockKongFeatureBuilder_JwtPlugin_Call{Call: _e.mock.On("JwtPlugin")} +} + +func (_c *MockKongFeatureBuilder_JwtPlugin_Call) Run(run func()) *MockKongFeatureBuilder_JwtPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_JwtPlugin_Call) Return(_a0 *plugin.JwtPlugin) *MockKongFeatureBuilder_JwtPlugin_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_JwtPlugin_Call) RunAndReturn(run func() *plugin.JwtPlugin) *MockKongFeatureBuilder_JwtPlugin_Call { + _c.Call.Return(run) + return _c +} + +// RateLimitPluginConsumeRoute provides a mock function with given fields: _a0 +func (_m *MockKongFeatureBuilder) RateLimitPluginConsumeRoute(_a0 *v1.ConsumeRoute) *plugin.RateLimitPlugin { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for RateLimitPluginConsumeRoute") + } + + var r0 *plugin.RateLimitPlugin + if rf, ok := ret.Get(0).(func(*v1.ConsumeRoute) *plugin.RateLimitPlugin); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.RateLimitPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateLimitPluginConsumeRoute' +type MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call struct { + *mock.Call +} + +// RateLimitPluginConsumeRoute is a helper method to define mock.On call +// - _a0 *v1.ConsumeRoute +func (_e *MockKongFeatureBuilder_Expecter) RateLimitPluginConsumeRoute(_a0 interface{}) *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call { + return &MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call{Call: _e.mock.On("RateLimitPluginConsumeRoute", _a0)} +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call) Run(run func(_a0 *v1.ConsumeRoute)) *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(*v1.ConsumeRoute)) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call) Return(_a0 *plugin.RateLimitPlugin) *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call) RunAndReturn(run func(*v1.ConsumeRoute) *plugin.RateLimitPlugin) *MockKongFeatureBuilder_RateLimitPluginConsumeRoute_Call { + _c.Call.Return(run) + return _c +} + +// RateLimitPluginRoute provides a mock function with no fields +func (_m *MockKongFeatureBuilder) RateLimitPluginRoute() *plugin.RateLimitPlugin { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RateLimitPluginRoute") + } + + var r0 *plugin.RateLimitPlugin + if rf, ok := ret.Get(0).(func() *plugin.RateLimitPlugin); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.RateLimitPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_RateLimitPluginRoute_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RateLimitPluginRoute' +type MockKongFeatureBuilder_RateLimitPluginRoute_Call struct { + *mock.Call +} + +// RateLimitPluginRoute is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) RateLimitPluginRoute() *MockKongFeatureBuilder_RateLimitPluginRoute_Call { + return &MockKongFeatureBuilder_RateLimitPluginRoute_Call{Call: _e.mock.On("RateLimitPluginRoute")} +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginRoute_Call) Run(run func()) *MockKongFeatureBuilder_RateLimitPluginRoute_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginRoute_Call) Return(_a0 *plugin.RateLimitPlugin) *MockKongFeatureBuilder_RateLimitPluginRoute_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_RateLimitPluginRoute_Call) RunAndReturn(run func() *plugin.RateLimitPlugin) *MockKongFeatureBuilder_RateLimitPluginRoute_Call { + _c.Call.Return(run) + return _c +} + +// RequestTransformerPlugin provides a mock function with no fields +func (_m *MockKongFeatureBuilder) RequestTransformerPlugin() *plugin.RequestTransformerPlugin { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RequestTransformerPlugin") + } + + var r0 *plugin.RequestTransformerPlugin + if rf, ok := ret.Get(0).(func() *plugin.RequestTransformerPlugin); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.RequestTransformerPlugin) + } + } + + return r0 +} + +// MockKongFeatureBuilder_RequestTransformerPlugin_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RequestTransformerPlugin' +type MockKongFeatureBuilder_RequestTransformerPlugin_Call struct { + *mock.Call +} + +// RequestTransformerPlugin is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) RequestTransformerPlugin() *MockKongFeatureBuilder_RequestTransformerPlugin_Call { + return &MockKongFeatureBuilder_RequestTransformerPlugin_Call{Call: _e.mock.On("RequestTransformerPlugin")} +} + +func (_c *MockKongFeatureBuilder_RequestTransformerPlugin_Call) Run(run func()) *MockKongFeatureBuilder_RequestTransformerPlugin_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_RequestTransformerPlugin_Call) Return(_a0 *plugin.RequestTransformerPlugin) *MockKongFeatureBuilder_RequestTransformerPlugin_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_RequestTransformerPlugin_Call) RunAndReturn(run func() *plugin.RequestTransformerPlugin) *MockKongFeatureBuilder_RequestTransformerPlugin_Call { + _c.Call.Return(run) + return _c +} + +// RoutingConfigs provides a mock function with no fields +func (_m *MockKongFeatureBuilder) RoutingConfigs() *plugin.RoutingConfigs { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for RoutingConfigs") + } + + var r0 *plugin.RoutingConfigs + if rf, ok := ret.Get(0).(func() *plugin.RoutingConfigs); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*plugin.RoutingConfigs) + } + } + + return r0 +} + +// MockKongFeatureBuilder_RoutingConfigs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RoutingConfigs' +type MockKongFeatureBuilder_RoutingConfigs_Call struct { + *mock.Call +} + +// RoutingConfigs is a helper method to define mock.On call +func (_e *MockKongFeatureBuilder_Expecter) RoutingConfigs() *MockKongFeatureBuilder_RoutingConfigs_Call { + return &MockKongFeatureBuilder_RoutingConfigs_Call{Call: _e.mock.On("RoutingConfigs")} +} + +func (_c *MockKongFeatureBuilder_RoutingConfigs_Call) Run(run func()) *MockKongFeatureBuilder_RoutingConfigs_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockKongFeatureBuilder_RoutingConfigs_Call) Return(_a0 *plugin.RoutingConfigs) *MockKongFeatureBuilder_RoutingConfigs_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockKongFeatureBuilder_RoutingConfigs_Call) RunAndReturn(run func() *plugin.RoutingConfigs) *MockKongFeatureBuilder_RoutingConfigs_Call { + _c.Call.Return(run) + return _c +} + +// SetUpstream provides a mock function with given fields: _a0 +func (_m *MockKongFeatureBuilder) SetUpstream(_a0 client.Upstream) { + _m.Called(_a0) +} + +// MockKongFeatureBuilder_SetUpstream_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetUpstream' +type MockKongFeatureBuilder_SetUpstream_Call struct { + *mock.Call +} + +// SetUpstream is a helper method to define mock.On call +// - _a0 client.Upstream +func (_e *MockKongFeatureBuilder_Expecter) SetUpstream(_a0 interface{}) *MockKongFeatureBuilder_SetUpstream_Call { + return &MockKongFeatureBuilder_SetUpstream_Call{Call: _e.mock.On("SetUpstream", _a0)} +} + +func (_c *MockKongFeatureBuilder_SetUpstream_Call) Run(run func(_a0 client.Upstream)) *MockKongFeatureBuilder_SetUpstream_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(client.Upstream)) + }) + return _c +} + +func (_c *MockKongFeatureBuilder_SetUpstream_Call) Return() *MockKongFeatureBuilder_SetUpstream_Call { + _c.Call.Return() + return _c +} + +func (_c *MockKongFeatureBuilder_SetUpstream_Call) RunAndReturn(run func(client.Upstream)) *MockKongFeatureBuilder_SetUpstream_Call { + _c.Run(run) + return _c +} + +// NewMockKongFeatureBuilder creates a new instance of MockKongFeatureBuilder. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockKongFeatureBuilder(t interface { + mock.TestingT + Cleanup(func()) +}) *MockKongFeatureBuilder { + mock := &MockKongFeatureBuilder{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/gateway/internal/features/util.go b/gateway/internal/features/util.go new file mode 100644 index 000000000..a11da56d9 --- /dev/null +++ b/gateway/internal/features/util.go @@ -0,0 +1,25 @@ +// Copyright 2026 Deutsche Telekom IT GmbH +// +// SPDX-License-Identifier: Apache-2.0 + +package features + +import "sort" + +// SortFeatures based on their priority +// the higher the priority, the later the feature is applied +// this is important because some features might depend on other features +func SortFeatures[T FeatureBuilder](featureList []Feature[T]) []Feature[T] { + sort.Slice(featureList, func(i, j int) bool { + return featureList[i].Priority() < featureList[j].Priority() + }) + return featureList +} + +func ToSlice[K comparable, T any](m map[K]T) []T { + s := make([]T, 0, len(m)) + for _, v := range m { + s = append(s, v) + } + return s +} diff --git a/gateway/internal/handler/consumer/handler.go b/gateway/internal/handler/consumer/handler.go index 55aef7a35..42ab905be 100644 --- a/gateway/internal/handler/consumer/handler.go +++ b/gateway/internal/handler/consumer/handler.go @@ -13,7 +13,8 @@ import ( "github.com/telekom/controlplane/common/pkg/handler" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" "github.com/telekom/controlplane/gateway/internal/handler/gateway" "github.com/telekom/controlplane/gateway/pkg/kongutil" ) @@ -23,7 +24,22 @@ var _ handler.Handler[*gatewayv1.Consumer] = &ConsumerHandler{} type ConsumerHandler struct{} func (h *ConsumerHandler) CreateOrUpdate(ctx context.Context, consumer *gatewayv1.Consumer) error { - builder, err := NewFeatureBuilder(ctx, consumer) + ready, gw, err := gateway.GetGatewayByRef(ctx, consumer.Spec.Gateway, true) + if err != nil { + return err + } + if !ready { + return ctrlerrors.BlockedErrorf("gateway %s is not ready", consumer.Spec.Gateway.Name) + } + + // Envoy has no notion of consumers. Report Ready (owners may depend on it) and skip. + if gw.Spec.GatewayClassName == gatewayv1.GatewayClassNameEnvoy { + consumer.SetCondition(condition.NewDoneProcessingCondition("Consumer is not supported for Envoy gateway")) + consumer.SetCondition(condition.NewReadyCondition("ConsumerReady", "Consumer is ready")) + return nil + } + + builder, err := NewFeatureBuilder(ctx, gw, consumer) if err != nil { return errors.Wrap(err, "failed to create feature builder") } @@ -45,6 +61,11 @@ func (h *ConsumerHandler) Delete(ctx context.Context, consumer *gatewayv1.Consum return err } + // Envoy has no notion of consumers; nothing was created, nothing to delete. + if gateway.Spec.GatewayClassName == gatewayv1.GatewayClassNameEnvoy { + return nil + } + kc, err := kongutil.GetClientFor(gateway) if err != nil { return errors.Wrap(err, "failed to get kong client") @@ -58,22 +79,14 @@ func (h *ConsumerHandler) Delete(ctx context.Context, consumer *gatewayv1.Consum return nil } -func NewFeatureBuilder(ctx context.Context, consumer *gatewayv1.Consumer) (features.FeaturesBuilder, error) { - - ready, gateway, err := gateway.GetGatewayByRef(ctx, consumer.Spec.Gateway, true) - if err != nil { - return nil, err - } - if !ready { - return nil, ctrlerrors.BlockedErrorf("gateway %s is not ready", consumer.Spec.Gateway.Name) - } +func NewFeatureBuilder(ctx context.Context, gateway *gatewayv1.Gateway, consumer *gatewayv1.Consumer) (features.KongFeatureBuilder, error) { kc, err := kongutil.GetClientFor(gateway) if err != nil { return nil, errors.Wrap(err, "failed to get kong client") } - builder := features.NewFeatureBuilder(kc, nil, consumer, gateway) + builder := kong.NewFeatureBuilder(kc, nil, consumer, gateway) builder.EnableFeature(feature.InstanceIpRestrictionFeature) return builder, nil diff --git a/gateway/internal/handler/consumer/handler_test.go b/gateway/internal/handler/consumer/handler_test.go index 82443be18..2533d9427 100644 --- a/gateway/internal/handler/consumer/handler_test.go +++ b/gateway/internal/handler/consumer/handler_test.go @@ -22,6 +22,7 @@ import ( "github.com/telekom/controlplane/common/pkg/types" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" + kongfeatures "github.com/telekom/controlplane/gateway/internal/features/kong" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" consumerhandler "github.com/telekom/controlplane/gateway/internal/handler/consumer" kongclient "github.com/telekom/controlplane/gateway/pkg/kong/client" @@ -36,7 +37,7 @@ var _ = Describe("ConsumerHandler", func() { handler *consumerhandler.ConsumerHandler mockClient *fakeclient.MockJanitorClient mockKC *clientmock.MockKongClient - mockBuilder *featmock.MockFeaturesBuilder + mockBuilder *featmock.MockKongFeatureBuilder consumer *gatewayv1.Consumer ) @@ -44,7 +45,7 @@ var _ = Describe("ConsumerHandler", func() { handler = &consumerhandler.ConsumerHandler{} mockClient = fakeclient.NewMockJanitorClient(GinkgoT()) mockKC = clientmock.NewMockKongClient(GinkgoT()) - mockBuilder = featmock.NewMockFeaturesBuilder(GinkgoT()) + mockBuilder = featmock.NewMockKongFeatureBuilder(GinkgoT()) ctx = cc.WithClient(context.Background(), mockClient) @@ -77,9 +78,9 @@ var _ = Describe("ConsumerHandler", func() { return mockKC, nil } - originalNewFeatureBuilder := features.NewFeatureBuilder - DeferCleanup(func() { features.NewFeatureBuilder = originalNewFeatureBuilder }) - features.NewFeatureBuilder = func(_ kongclient.KongClient, _ *gatewayv1.Route, _ *gatewayv1.Consumer, _ *gatewayv1.Gateway) features.FeaturesBuilder { + originalNewFeatureBuilder := kongfeatures.NewFeatureBuilder + DeferCleanup(func() { kongfeatures.NewFeatureBuilder = originalNewFeatureBuilder }) + kongfeatures.NewFeatureBuilder = func(_ kongclient.KongClient, _ *gatewayv1.Route, _ *gatewayv1.Consumer, _ *gatewayv1.Gateway) features.KongFeatureBuilder { return mockBuilder } } @@ -134,7 +135,7 @@ var _ = Describe("ConsumerHandler", func() { It("returns error when feature builder creation fails (gateway not ready)", func() { setupNotReadyGatewayGet() - // Don't override features.NewFeatureBuilder - let the real one see the not-ready gateway + // Don't override kongfeatures.NewFeatureBuilder - let the real one see the not-ready gateway originalGetClientFor := kongutil.GetClientFor DeferCleanup(func() { kongutil.GetClientFor = originalGetClientFor }) kongutil.GetClientFor = func(_ kongutil.GatewayAdminConfig) (kongclient.KongClient, error) { diff --git a/gateway/internal/handler/route/handler.go b/gateway/internal/handler/route/handler.go index b7f9d22c2..ff807085e 100644 --- a/gateway/internal/handler/route/handler.go +++ b/gateway/internal/handler/route/handler.go @@ -18,7 +18,10 @@ import ( "github.com/telekom/controlplane/common/pkg/types" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" - "github.com/telekom/controlplane/gateway/internal/features/feature" + "github.com/telekom/controlplane/gateway/internal/features/envoy" + envoyfeature "github.com/telekom/controlplane/gateway/internal/features/envoy/feature" + "github.com/telekom/controlplane/gateway/internal/features/kong" + "github.com/telekom/controlplane/gateway/internal/features/kong/feature" "github.com/telekom/controlplane/gateway/internal/handler/gateway" "github.com/telekom/controlplane/gateway/pkg/kongutil" "sigs.k8s.io/controller-runtime/pkg/client" @@ -27,12 +30,17 @@ import ( var _ handler.Handler[*gatewayv1.Route] = &RouteHandler{} -type RouteHandler struct{} +type RouteHandler struct { + // XdsClient is the shared, long-lived Envoy xDS client. It is only used for + // routes on an Envoy-class Gateway; the Kong path ignores it. May be nil in + // Kong-only setups. + XdsClient envoy.XdsClient +} func (h *RouteHandler) CreateOrUpdate(ctx context.Context, route *gatewayv1.Route) error { log := log.FromContext(ctx) kubeClient := cc.ClientFromContextOrDie(ctx) - builder, err := NewFeatureBuilder(ctx, route) + builder, err := h.NewFeatureBuilder(ctx, route) if err != nil { return errors.Wrap(err, "failed to create feature builder") } @@ -126,7 +134,7 @@ func (h *RouteHandler) Delete(ctx context.Context, route *gatewayv1.Route) error return nil } -func NewFeatureBuilder(ctx context.Context, route *gatewayv1.Route) (features.FeaturesBuilder, error) { +func (h *RouteHandler) NewFeatureBuilder(ctx context.Context, route *gatewayv1.Route) (features.FeatureBuilder, error) { ready, gateway, err := gateway.GetGatewayByRef(ctx, route.Spec.GatewayRef, true) if err != nil { @@ -136,12 +144,22 @@ func NewFeatureBuilder(ctx context.Context, route *gatewayv1.Route) (features.Fe return nil, ctrlerrors.BlockedErrorf("gateway %q is not ready", route.Spec.GatewayRef.Name) } + if gateway.Spec.GatewayClassName == gatewayv1.GatewayClassNameEnvoy { + builder := envoy.NewEnvoyFeatureBuilder(h.XdsClient, route, nil, gateway) + builder.EnableFeature(envoyfeature.InstanceAccessControlFeature) + + return builder, nil + + } + + // fallback to Kong as default to support existing installations that don't have the gatewayClassName set + kc, err := kongutil.GetClientFor(gateway) if err != nil { return nil, errors.Wrap(err, "failed to get kong client") } - builder := features.NewFeatureBuilder(kc, route, nil, gateway) + builder := kong.NewFeatureBuilder(kc, route, nil, gateway) builder.EnableFeature(feature.InstanceAccessControlFeature) builder.EnableFeature(feature.InstancePassThroughFeature) builder.EnableFeature(feature.InstanceLastMileSecurityFeature) @@ -155,6 +173,5 @@ func NewFeatureBuilder(ctx context.Context, route *gatewayv1.Route) (features.Fe builder.EnableFeature(feature.InstanceBasicAuthFeature) builder.EnableFeature(feature.InstanceCircuitBreakerFeature) builder.EnableFeature(feature.InstanceDynamicUpstreamFeature) - return builder, nil } diff --git a/gateway/internal/handler/route/handler_test.go b/gateway/internal/handler/route/handler_test.go index f8bb2a3dc..349ef339c 100644 --- a/gateway/internal/handler/route/handler_test.go +++ b/gateway/internal/handler/route/handler_test.go @@ -22,6 +22,7 @@ import ( "github.com/telekom/controlplane/common/pkg/types" gatewayv1 "github.com/telekom/controlplane/gateway/api/v1" "github.com/telekom/controlplane/gateway/internal/features" + kongfeatures "github.com/telekom/controlplane/gateway/internal/features/kong" featmock "github.com/telekom/controlplane/gateway/internal/features/mock" routehandler "github.com/telekom/controlplane/gateway/internal/handler/route" kongclient "github.com/telekom/controlplane/gateway/pkg/kong/client" @@ -36,7 +37,7 @@ var _ = Describe("RouteHandler", func() { handler *routehandler.RouteHandler mockClient *fakeclient.MockJanitorClient mockKC *clientmock.MockKongClient - mockBuilder *featmock.MockFeaturesBuilder + mockBuilder *featmock.MockKongFeatureBuilder route *gatewayv1.Route ) @@ -44,7 +45,7 @@ var _ = Describe("RouteHandler", func() { handler = &routehandler.RouteHandler{} mockClient = fakeclient.NewMockJanitorClient(GinkgoT()) mockKC = clientmock.NewMockKongClient(GinkgoT()) - mockBuilder = featmock.NewMockFeaturesBuilder(GinkgoT()) + mockBuilder = featmock.NewMockKongFeatureBuilder(GinkgoT()) ctx = cc.WithClient(context.Background(), mockClient) @@ -73,7 +74,7 @@ var _ = Describe("RouteHandler", func() { }) // setupGatewayMocks configures the mock client Get to return a ready gateway - // and overrides kongutil.GetClientFor and features.NewFeatureBuilder. + // and overrides kongutil.GetClientFor and kongfeatures.NewFeatureBuilder. setupFeatureBuilderOverrides := func() { originalGetClientFor := kongutil.GetClientFor DeferCleanup(func() { kongutil.GetClientFor = originalGetClientFor }) @@ -81,9 +82,9 @@ var _ = Describe("RouteHandler", func() { return mockKC, nil } - originalNewFeatureBuilder := features.NewFeatureBuilder - DeferCleanup(func() { features.NewFeatureBuilder = originalNewFeatureBuilder }) - features.NewFeatureBuilder = func(_ kongclient.KongClient, _ *gatewayv1.Route, _ *gatewayv1.Consumer, _ *gatewayv1.Gateway) features.FeaturesBuilder { + originalNewFeatureBuilder := kongfeatures.NewFeatureBuilder + DeferCleanup(func() { kongfeatures.NewFeatureBuilder = originalNewFeatureBuilder }) + kongfeatures.NewFeatureBuilder = func(_ kongclient.KongClient, _ *gatewayv1.Route, _ *gatewayv1.Consumer, _ *gatewayv1.Gateway) features.KongFeatureBuilder { return mockBuilder } } @@ -218,7 +219,7 @@ var _ = Describe("RouteHandler", func() { It("returns error when NewFeatureBuilder fails (gateway not ready)", func() { setupNotReadyGatewayGet() - // Don't override features.NewFeatureBuilder - let the real one run with the not-ready gateway + // Don't override kongfeatures.NewFeatureBuilder - let the real one run with the not-ready gateway originalGetClientFor := kongutil.GetClientFor DeferCleanup(func() { kongutil.GetClientFor = originalGetClientFor }) kongutil.GetClientFor = func(_ kongutil.GatewayAdminConfig) (kongclient.KongClient, error) { diff --git a/gateway/pkg/kong/client/mock/mock_KongAdminApi.go b/gateway/pkg/kong/client/mock/mock_KongAdminApi.go index 33a8fd1ff..3ac7f8506 100644 --- a/gateway/pkg/kong/client/mock/mock_KongAdminApi.go +++ b/gateway/pkg/kong/client/mock/mock_KongAdminApi.go @@ -1,4 +1,4 @@ -// Copyright 2026 Deutsche Telekom IT GmbH +// SPDX-FileCopyrightText: 2025 Deutsche Telekom IT GmbH // // SPDX-License-Identifier: Apache-2.0 diff --git a/gateway/pkg/kong/client/mock/mock_KongClient.go b/gateway/pkg/kong/client/mock/mock_KongClient.go index a203fa3a3..05f2ec319 100644 --- a/gateway/pkg/kong/client/mock/mock_KongClient.go +++ b/gateway/pkg/kong/client/mock/mock_KongClient.go @@ -1,4 +1,4 @@ -// Copyright 2026 Deutsche Telekom IT GmbH +// SPDX-FileCopyrightText: 2025 Deutsche Telekom IT GmbH // // SPDX-License-Identifier: Apache-2.0 diff --git a/gateway/tools/mockery.yaml b/gateway/tools/mockery.yaml index 93ec0eb7d..e35e3e8fc 100644 --- a/gateway/tools/mockery.yaml +++ b/gateway/tools/mockery.yaml @@ -5,6 +5,7 @@ with-expecter: True mockname: "Mock{{.InterfaceName}}" outpkg: "mock" +boilerplate-file: "../../hack/boilerplate.go.txt" filename: "mock_{{.InterfaceName}}.go" packages: github.com/telekom/controlplane/gateway/pkg/kong/client: @@ -25,7 +26,7 @@ packages: config: interfaces: # select the interfaces you want mocked - FeaturesBuilder: + KongFeatureBuilder: # Modify package-level config for this specific interface (if applicable) config: dir: "../internal/features/mock"