Skip to content

[feat] support Kubernetes Gateway API - #6347

Draft
eye-gu wants to merge 6 commits into
apache:masterfrom
eye-gu:fix-6346
Draft

[feat] support Kubernetes Gateway API#6347
eye-gu wants to merge 6 commits into
apache:masterfrom
eye-gu:fix-6346

Conversation

@eye-gu

@eye-gu eye-gu commented May 18, 2026

Copy link
Copy Markdown
Member

close #6346

Implements ShenYu support for Kubernetes Gateway API (gateway.networking.k8s.io/v1), complementing the existing Ingress support.

Core Components

Component Description
GatewayClassReconciler Watches GatewayClass, accepts those with spec.controllerName=shenyu
GatewayReconciler Watches Gateway, re-queues affected HTTPRoutes on Gateway changes
HTTPRouteReconciler Watches HTTPRoute, parses into ShenYu selector/rule via HttpRouteParser
HttpRouteParser Translates HTTPRoute spec (hostnames + matches + backendRefs) into SelectorData/RuleData
GatewayRouteCache Thread-safe cache for route↔selector and gateway↔route bindings
GatewayApiControllerConfiguration Spring Boot auto-configuration for all Gateway API beans

Make sure that:

  • You have read the contribution guidelines.
  • You submit test cases (unit or integration tests) that back your changes.
  • Your local test passed ./mvnw clean install -Dmaven.javadoc.skip=true.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Kubernetes Gateway API (gateway.networking.k8s.io/v1) support to ShenYu’s k8s controller/starter, alongside existing Ingress support, and introduces a new integrated test workflow to validate Gateway API routing.

Changes:

  • Adds Spring Boot auto-configuration and controllers/reconcilers for GatewayClass, Gateway, and HTTPRoute.
  • Implements HttpRouteParser + GatewayRouteCache to translate HTTPRoute specs into ShenYu selector/rule config and track bindings.
  • Introduces a new k8s Gateway API integrated test module and GitHub Actions workflow to run it on kind.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports Registers the new Gateway API auto-configuration.
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/resources/META-INF/spring.factories Registers Gateway API auto-configuration for legacy Spring Boot loading.
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java Adds shenyu.k8s.mode gating and fixes default secret TLS loading condition.
shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java New Gateway API controller wiring (informers/controllers/reconcilers/repository bootstrap).
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayClassReconciler.java New GatewayClass reconciliation + status patch + requeue logic.
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java New Gateway reconciliation, status patching, and HTTPRoute requeueing.
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java New HTTPRoute reconciliation, config apply/delete, binding, and status patching.
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java New HTTPRoute→selector/rule translation logic.
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/GatewayApiConstants.java Gateway API constants + shared condition helper.
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/GatewayRouteCache.java New thread-safe cache for route↔selector and gateway↔route bindings.
shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/GatewayReconcilerTest.java Unit tests for Gateway reconciliation behaviors.
shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HTTPRouteReconcilerTest.java Unit tests for HTTPRoute reconciliation behaviors.
shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/HttpRouteParserTest.java Unit tests for HTTPRoute parsing/mapping logic.
shenyu-integrated-test/pom.xml Adds the new Gateway API integrated test module to the build.
shenyu-integrated-test/shenyu-integrated-test-k8s-gateway-api-http/** New integrated test module (app, config, Dockerfile, kind manifests, scripts, tests).
shenyu-examples/shenyu-examples-http/k8s/gateway-api.yml Example GatewayClass/Gateway/HTTPRoute manifests for the HTTP example.
.github/workflows/integrated-test-k8s-gateway-api.yml New CI workflow to run Gateway API integrated tests on kind.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Comment thread shenyu-examples/shenyu-examples-http/k8s/gateway-api.yml

@Aias00 Aias00 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this — the Gateway API support is a solid addition and the test scaffolding is appreciated. A few correctness/operational issues before merge:

Blocker — non-idempotent HTTPRoute reconcile. HTTPRouteReconciler.reconcile (HTTPRouteReconciler.java:2192-2195) unconditionally does deleteConfigparseapplyConfig on every pass, and HttpRouteParser allocates fresh selector/rule IDs from a monotonic AtomicLong on each parse (GatewayRouteCache.java:1127-1133). Because the informer resyncs every 1 min (GatewayApiControllerConfiguration, withResyncPeriod(Duration.ofMinutes(1))), every HTTPRoute is re-reconciled each minute: the old selector/rule IDs are deleted and new ones created even when the spec is unchanged. ShenyuCacheRepository.saveOrUpdateSelectorData then pushes delete+create into the live data-plane cache, so there's a brief window per route per minute where matching requests find no selector. The Ingress reconciler avoids this by only re-applying when needUpdate(old, current) is true (IngressReconciler.java:175). Suggest either reusing deterministic IDs derived from (namespace, routeName, ruleIndex, hostname) or skipping re-apply when the parsed config is unchanged, and adding a test that asserts IDs are stable across two reconcile() calls.

Blocker — own e2e is red. The new it-k8s-gateway-api workflow fails on this head (build (shenyu-integrated-test-k8s-gateway-api-http) FAILURE, run 29428497010). Please grab the controller logs from the workflow's debug step and fix or explain.

Should fix.

  • GatewayReconciler.isShenyuGateway compares spec.gatewayClassName to the literal "shenyu" instead of resolving the GatewayClass and checking its spec.controllerName (GatewayClassReconciler already has the correct check). Gateways whose GatewayClass has any other name are silently ignored even when that class is ShenYu-owned.
  • Cross-namespace parentRefs are accepted without a ReferenceGrant check, and ResolvedRefs=True is reported unconditionally — including when backend endpoints are missing/unresolvable (HttpRouteParser.parseBackendRefs logs and skips, then updateHTTPRouteStatus still emits ResolvedRefs=True). The latter also programs a divide selector with handle="[]", so requests 5xx while status claims healthy.
  • bindToGateway binds to non-ShenYu parents too, and the Gateway deletion path calls deleteAssociatedRoutes without verifying the deleted Gateway was ShenYu-managed — deleting a non-ShenYu gateway of the same name can wipe selectors for a route still served by a ShenYu gateway.
  • GatewayReconciler.requeueAffectedHTTPRoutes does a full cluster-wide HTTPRoute scan on every (resync) reconcile; should only run when the Gateway actually changed.
  • No leader election; multi-replica deployments will race on status patches and double-reconcile. Either wire leader election or document single-replica-only.
  • Wildcard hostnames (*.example.com) are matched with OperatorEnum.EQ (HttpRouteParser.processRule:1340-1348) — they will never match subdomains. Please emit a domain pattern operator for wildcard hostnames.
  • GatewayClassReconciler.updateGatewayClassAcceptedStatus rebuilds the conditions array from scratch; merge-patch replaces arrays wholesale, so it clobbers conditions set by other controllers. Mirror the preservation logic already used for Gateway status.

Minor: the MapUtils.isEmptyisNotEmpty fix in IngressControllerConfiguration.tcpSslContextSpec is a real and correct bug fix (current master is inverted), but it's unrelated to the Gateway API feature — please split into its own commit/PR for traceability.

For reference I reviewed the full diff (head 19e8bd8) plus the existing IngressReconciler/ShenyuCacheRepository in the local tree; did not modify anything.

eye-gu added a commit to eye-gu/shenyu that referenced this pull request Aug 5, 2026
Addresses Aias00's review on apache#6347 (CHANGES_REQUESTED).

Blockers:
- CI: align gateway-api test module parent version to 2.7.2-SNAPSHOT
  (was 2.7.1-SNAPSHOT, causing 'Non-resolvable parent POM' in workflow)
- HTTPRoute reconcile idempotency: replace AtomicLong IDs with
  deterministic name-based UUIDs derived from (namespace, routeName,
  ruleIndex, hostname, matchIndex). Reconciler now diffs old vs new
  selector IDs and deletes only stale ones, so informer resync no
  longer churns the data plane.

Should fix:
- isShenyuGateway now resolves GatewayClass.spec.controllerName instead
  of comparing gatewayClassName to literal 'shenyu'
- bindToGateway only binds ShenYu-managed parents; Gateway deletion
  cascade is safe since non-ShenYu gateways never enter the cache
- requeueAffectedHTTPRoutes runs only on first Gateway Accept, not on
  every resync (avoids full-cluster HTTPRoute scan)
- leader election via LeaderElectingController + Lease, with RBAC for
  coordination.k8s.io/leases and Downward API pod identity injection;
  toggleable via shenyu.k8s.leader-election.enabled (default true)
- wildcard hostnames (*.example.com) emit an anchored single-label
  subdomain regex with MATCH instead of EQ (which never matched)
- GatewayClass status conditions preserve non-Accepted conditions to
  avoid merge-patch clobbering other controllers

Tests: added stability/idempotency/wildcard coverage; 22 tests pass.

ReferenceGrant validation and ResolvedRefs accuracy (S#2) deferred to
a follow-up batch.
@eye-gu
eye-gu marked this pull request as draft August 5, 2026 12:04
@eye-gu
eye-gu requested review from Aias00 and removed request for Aias00 August 21, 2026 10:28
@eye-gu
eye-gu marked this pull request as ready for review August 21, 2026 13:50
@eye-gu
eye-gu marked this pull request as draft August 21, 2026 15:41
eye-gu added 3 commits August 31, 2026 18:59
BaseDataCache.obtainRuleData returns null when the selector id has no
cached rules yet. ShenyuCacheRepository.saveOrUpdateRuleData and
IngressReconciler.deleteSelectorByIngressName consume the result
unguarded, so the first reconcile of a route (rule written before any
rule of its selector is cached) or a cascading delete threw NPE,
aborting the reconciler loop and leaving the gateway without routing
data, which failed the k8s gateway-api and ingress integration tests.
Make findRuleDataList fall back to an empty list and add regression
tests.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 40 out of 40 changed files in this pull request and generated 7 comments.

Suppressed comments (13)

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java:526

  • An explicit PathPrefix: / is compiled as ^\Q/\E(/.*)?$, which does not match normal paths such as /order (only / or paths beginning with //). Since / is the Gateway API catch-all prefix, these routes become unreachable. Special-case the root prefix so it matches every absolute path.
        return "^" + Pattern.quote(stripped) + "(/.*)?$";

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:236

  • ParentReference.port is ignored, so a parentRef selecting port 443 can attach through an unrelated listener selected only by sectionName (or through every listener when sectionName is absent). Read the parent port and require selected listeners to match both optional selectors; also carry the port into the emitted RouteParentStatus parentRef.
            String sectionName = JsonFields.getString(parentRef, "sectionName");
            decisions.add(evaluateListeners(gateway, sectionName, routeNamespace, parentNamespace, parentName,
                    routeHostnames, parentRef));

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:167

  • An empty hostname list is documented as “any host”, but unioning it as an ordinary empty set loses that meaning. If one accepted parent is unrestricted and another contributes example.com, this returns only example.com, incorrectly restricting a route that should remain host-agnostic. Return the any-host sentinel as soon as any accepted parent has an empty hostname list.
    private List<String> effectiveHostnames(final List<ParentDecision> accepted) {
        Set<String> union = new LinkedHashSet<>();
        for (ParentDecision decision : accepted) {
            union.addAll(decision.hostnames);
        }
        return new ArrayList<>(union);

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java:451

  • A valid multi-port Service cannot generally be resolved from the set of endpoint target ports alone. For example, service port 80 targeting 8080 alongside another port targeting 8443 yields {8080,8443}, so this returns null even though the reference is valid. Watch/lister Service objects and map the requested Service port (including named targetPorts) to its target port, as the existing Ingress path does.
        if (endpointPorts.size() == 1) {
            return endpointPorts.iterator().next();
        }
        if (Objects.nonNull(servicePort) && (endpointPorts.isEmpty() || endpointPorts.contains(servicePort))) {
            return servicePort;

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:131

  • Routes are requeued only when the Gateway transitions from not accepted to accepted. Listener edits normally leave Gateway-level Accepted=True, so previously programmed routes continue using stale hostname/allowedRoutes/protocol/port policy until the one-minute HTTPRoute resync; a revoked namespace policy can therefore remain active. Requeue affected routes whenever the Gateway spec generation changes.
            if (!gatewayStatusMatches(gateway, desiredStatus)) {
                patchGatewayStatus(gateway, desiredStatus);
            }
            if (!wasAccepted) {
                requeueAffectedHTTPRoutes(request.getNamespace(), request.getName());

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:295

  • Every listener receives the Gateway-wide route count, but attachedRoutes is defined per listener. A route targeting only sectionName: http is therefore also counted on unrelated listeners. Track bindings at listener granularity and compute each listener's count from routes that actually attach to it.
            supportedKinds.add(kind);
            listenerStatus.add("supportedKinds", supportedKinds);
            listenerStatus.addProperty("attachedRoutes", attachedRoutes);

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:511

  • Status equality ignores observedGeneration. If the HTTPRoute spec changes but the condition type/status/reason stays the same, the existing parent status is treated as current and never patched, leaving consumers unable to tell that the new generation was observed. Include observedGeneration in the condition comparison.
                if (Objects.equals(JsonFields.getString(existingCondition, "type"), JsonFields.getString(desiredCondition, "type"))
                        && Objects.equals(JsonFields.getString(existingCondition, "status"), JsonFields.getString(desiredCondition, "status"))
                        && reasonMatches) {

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayClassReconciler.java:172

  • Returning solely because any Accepted=True condition exists leaves its observedGeneration stale after the GatewayClass spec generation changes. Only skip the patch when the Accepted condition's observedGeneration equals the current metadata generation.
    private void updateGatewayClassAcceptedStatus(final DynamicKubernetesObject gatewayClass) {
        if (GatewayApiConstants.isConditionTrue(gatewayClass, "Accepted")) {
            return;

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java:418

  • Dividing each backend weight independently does not preserve the backendRef ratios. The added 9:1 test with two endpoints per backend actually produces totals 8:2 (4:1), and a weight-1 backend with many replicas can outweigh a weight-9 backend because every endpoint is floored to 1. Normalize endpoint weights across all backendRefs so each backend's aggregate weight remains proportional to its declared weight.
        if (weight == 0) {
            return BackendRefOutcome.ok(List.of());
        }
        int perEndpointWeight = Math.max(1, weight / readyIps.size());

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:266

  • Listener attachment ignores whether the listener port is actually served. Consequently an HTTP listener on port 8080 is accepted and its route is programmed even though GatewayReconciler reports that same listener as PortUnavailable when server.port is 9195. Include the served-port check in route listener evaluation so status and data-plane behavior agree.
        for (JsonObject listener : selected) {
            if (!ListenerSupport.isSupportedProtocol(listener)) {
                continue;
            }
            if (!ListenerSupport.allowsNamespace(listener, routeNamespace, parentNamespace)

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:361

  • These Long values are compared by reference. Counts above the JVM boxing cache range (commonly 127) compare unequal even when numerically identical, causing the controller to patch status on every reconcile and retrigger its watch. Use value equality.
            if (Objects.isNull(existingListener)
                    || JsonFields.getLong(existingListener, "attachedRoutes")
                    != JsonFields.getLong(desiredListener, "attachedRoutes")
                    || !conditionsMatch(JsonFields.getJsonArray(existingListener, "conditions"),
                    desiredListener.getAsJsonArray("conditions"))) {

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:399

  • Gateway status matching ignores observedGeneration, so a spec update that leaves condition states unchanged never advances the conditions to the new generation. Compare observedGeneration alongside type/status/reason; this still avoids patches on status-only events while accurately acknowledging spec changes.
                if (Objects.equals(JsonFields.getString(existingCondition, "type"), JsonFields.getString(desiredCondition, "type"))
                        && Objects.equals(JsonFields.getString(existingCondition, "status"), JsonFields.getString(desiredCondition, "status"))
                        && reasonMatches) {

shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:302

  • Whenever attachedRoutes changes, the status patch rebuilds these unchanged Accepted/Programmed conditions with a fresh lastTransitionTime. Kubernetes condition timestamps should advance only when the condition status transitions, not for an unrelated count update. Preserve existing transition times for conditions whose type and status are unchanged, as the HTTPRoute reconciler already does.
                listenerConditions.add(buildCondition(GatewayApiConstants.CONDITION_ACCEPTED, "True",
                        GatewayApiConstants.CONDITION_ACCEPTED, "Listener is accepted", generation));
                listenerConditions.add(buildCondition(GatewayApiConstants.CONDITION_PROGRAMMED, "True",
                        GatewayApiConstants.REASON_PROGRAMMED, "Listener is programmed", generation));

Comment on lines +96 to +99
public static boolean isServiceRef(final JsonObject backendRef) {
String kind = JsonFields.getString(backendRef, "kind");
return Objects.isNull(kind) || SERVICE_KIND.equals(kind);
}
Comment on lines +83 to +85
if (!isShenyuGatewayClass(gatewayClass)) {
LOG.info("GatewayClass {} is not managed by ShenYu, skipping", request.getName());
return new Result(false);
Comment on lines +337 to +341
BackendRefOutcome outcome = resolveBackendRef(element.getAsJsonObject(), namespace, routeName);
if (Objects.nonNull(outcome.unresolvedReason)) {
unresolvedCount++;
if (Objects.isNull(unresolvedReason)) {
unresolvedReason = outcome.unresolvedReason;
Comment on lines +49 to +53
if (ready) {
return true;
}
ready = informers.stream().allMatch(SharedIndexInformer::hasSynced);
return ready;
Comment on lines +129 to +132
Set<String> newUrls = upstreamDataList.stream().map(DiscoveryUpstreamData::getUrl).collect(Collectors.toSet());
List<Upstream> cachedUpstreams = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorData.getId());
Set<String> cachedUrls = CollectionUtils.isEmpty(cachedUpstreams) ? Collections.emptySet()
: cachedUpstreams.stream().map(Upstream::getUrl).collect(Collectors.toSet());
Comment on lines +110 to +113
if (CollectionUtils.isEmpty(GatewayRouteCache.getInstance()
.getRoutesByGateway(request.getNamespace(), request.getName()))) {
LOG.debug("Gateway {} is not managed by ShenYu, skipping", request);
return new Result(false);
Comment on lines +135 to +142
List<String> newSelectorIds = selectorIdsOf(config);
// read the previous snapshot before putRouteSelectors overwrites it
List<String> oldSelectorIds = Objects.requireNonNullElse(
cache.getRouteSelectors(namespace, routeName, PluginEnum.DIVIDE.getName()), List.of());
cache.putRouteSelectors(namespace, routeName, PluginEnum.DIVIDE.getName(), newSelectorIds);
deleteStaleSelectors(namespace, routeName, oldSelectorIds, newSelectorIds);
applyConfig(config, namespace, routeName);
rebindGateways(cache, namespace, routeName, accepted);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Support Kubernetes Gateway API

3 participants