Copilot commented on code in PR #6347: URL: https://github.com/apache/shenyu/pull/6347#discussion_r3910291333
########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,713 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.k8s.reconciler; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Result; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.k8s.cache.GatewayRouteCache; +import org.apache.shenyu.k8s.common.GatewayApiConstants; +import org.apache.shenyu.k8s.common.IngressConfiguration; +import org.apache.shenyu.k8s.common.JsonFields; +import org.apache.shenyu.k8s.common.ListenerSupport; +import org.apache.shenyu.k8s.common.ReferenceGrants; +import org.apache.shenyu.k8s.common.ShenyuMemoryConfig; +import org.apache.shenyu.k8s.common.StatusMergePatch; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Reconciler for HTTPRoute resources (Gateway API v1). + * + * <p>Every parentRef is evaluated individually and reported in status.parents — including + * rejections (no ReferenceGrant, listener policy, hostname mismatch, missing parent) with + * the spec-defined reason — except parentRefs resolved to Gateways owned by another + * controller, whose status entries belong to that controller. + */ +public class HTTPRouteReconciler implements Reconciler { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPRouteReconciler.class); + + private final Lister<DynamicKubernetesObject> httpRouteLister; + + private final Lister<DynamicKubernetesObject> gatewayLister; + + private final Lister<DynamicKubernetesObject> gatewayClassLister; + + private final Lister<DynamicKubernetesObject> referenceGrantLister; + + private final HttpRouteParser httpRouteParser; + + private final ShenyuCacheRepository shenyuCacheRepository; + + private final ApiClient apiClient; + + /** The port the embedded ShenYu data plane actually listens on ({@code server.port}); route attachment is allowed only on listeners the gateway really serves. */ + private final int servedPort; + + public HTTPRouteReconciler(final SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer, + final SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient, + final int servedPort) { + this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer()); + this.gatewayLister = new Lister<>(gatewayInformer.getIndexer()); + this.gatewayClassLister = new Lister<>(gatewayClassInformer.getIndexer()); + this.referenceGrantLister = new Lister<>(referenceGrantInformer.getIndexer()); + this.httpRouteParser = httpRouteParser; + this.shenyuCacheRepository = shenyuCacheRepository; + this.apiClient = apiClient; + this.servedPort = servedPort; + } + + @Override + public Result reconcile(final Request request) { + String namespace = request.getNamespace(); + String routeName = request.getName(); + LOG.debug("Starting to reconcile HTTPRoute {}/{}", namespace, routeName); + + DynamicKubernetesObject httpRoute = httpRouteLister.namespace(namespace).get(routeName); + if (Objects.isNull(httpRoute)) { + deleteConfig(namespace, routeName); + return new Result(false); + } + + List<ParentDecision> decisions = evaluateParents(httpRoute); + if (decisions.isEmpty()) { + // Not attached to any ShenYu-managed Gateway; drop previously programmed config + // and our status entries, then leave the route to other controllers. + deleteConfig(namespace, routeName); + removeShenyuParentStatus(httpRoute, namespace, routeName); + return new Result(false); + } + + List<ParentDecision> accepted = new ArrayList<>(); + for (ParentDecision decision : decisions) { + if (decision.accepted) { + accepted.add(decision); + } + } + + ShenyuMemoryConfig config = null; + if (accepted.isEmpty()) { + // All ShenYu parents rejected the attachment: clean up but still report status. + deleteConfig(namespace, routeName); + } else { + config = httpRouteParser.parse(httpRoute, effectiveHostnames(accepted)); Review Comment: A mixed valid/invalid weighted backend set is parsed into an empty-upstream selector, so applying this config makes 100% of matching requests fail. Gateway API requires only the invalid backend's weighted proportion to return HTTP 500 while valid backend shares remain routable. Preserve valid upstreams and represent the invalid weighted share as a failure target instead of failing the whole rule. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,713 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.k8s.reconciler; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Result; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.k8s.cache.GatewayRouteCache; +import org.apache.shenyu.k8s.common.GatewayApiConstants; +import org.apache.shenyu.k8s.common.IngressConfiguration; +import org.apache.shenyu.k8s.common.JsonFields; +import org.apache.shenyu.k8s.common.ListenerSupport; +import org.apache.shenyu.k8s.common.ReferenceGrants; +import org.apache.shenyu.k8s.common.ShenyuMemoryConfig; +import org.apache.shenyu.k8s.common.StatusMergePatch; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Reconciler for HTTPRoute resources (Gateway API v1). + * + * <p>Every parentRef is evaluated individually and reported in status.parents — including + * rejections (no ReferenceGrant, listener policy, hostname mismatch, missing parent) with + * the spec-defined reason — except parentRefs resolved to Gateways owned by another + * controller, whose status entries belong to that controller. + */ +public class HTTPRouteReconciler implements Reconciler { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPRouteReconciler.class); + + private final Lister<DynamicKubernetesObject> httpRouteLister; + + private final Lister<DynamicKubernetesObject> gatewayLister; + + private final Lister<DynamicKubernetesObject> gatewayClassLister; + + private final Lister<DynamicKubernetesObject> referenceGrantLister; + + private final HttpRouteParser httpRouteParser; + + private final ShenyuCacheRepository shenyuCacheRepository; + + private final ApiClient apiClient; + + /** The port the embedded ShenYu data plane actually listens on ({@code server.port}); route attachment is allowed only on listeners the gateway really serves. */ + private final int servedPort; + + public HTTPRouteReconciler(final SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer, + final SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient, + final int servedPort) { + this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer()); + this.gatewayLister = new Lister<>(gatewayInformer.getIndexer()); + this.gatewayClassLister = new Lister<>(gatewayClassInformer.getIndexer()); + this.referenceGrantLister = new Lister<>(referenceGrantInformer.getIndexer()); + this.httpRouteParser = httpRouteParser; + this.shenyuCacheRepository = shenyuCacheRepository; + this.apiClient = apiClient; + this.servedPort = servedPort; + } + + @Override + public Result reconcile(final Request request) { + String namespace = request.getNamespace(); + String routeName = request.getName(); + LOG.debug("Starting to reconcile HTTPRoute {}/{}", namespace, routeName); + + DynamicKubernetesObject httpRoute = httpRouteLister.namespace(namespace).get(routeName); + if (Objects.isNull(httpRoute)) { + deleteConfig(namespace, routeName); + return new Result(false); + } + + List<ParentDecision> decisions = evaluateParents(httpRoute); + if (decisions.isEmpty()) { + // Not attached to any ShenYu-managed Gateway; drop previously programmed config + // and our status entries, then leave the route to other controllers. + deleteConfig(namespace, routeName); + removeShenyuParentStatus(httpRoute, namespace, routeName); + return new Result(false); + } + + List<ParentDecision> accepted = new ArrayList<>(); + for (ParentDecision decision : decisions) { + if (decision.accepted) { + accepted.add(decision); + } + } + + ShenyuMemoryConfig config = null; + if (accepted.isEmpty()) { + // All ShenYu parents rejected the attachment: clean up but still report status. + deleteConfig(namespace, routeName); + } else { + config = httpRouteParser.parse(httpRoute, effectiveHostnames(accepted)); Review Comment: The generated selector ordering does not implement Gateway API match precedence. ShenYu first chooses the matching selector with the most AND conditions, so for equal paths a match with two headers beats a match with an HTTP method, while Gateway API gives method presence precedence before header count. Encode the full Gateway precedence independently of raw condition count to avoid routing overlapping rules to the wrong backend. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,713 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.k8s.reconciler; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Result; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.ApiException; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.k8s.cache.GatewayRouteCache; +import org.apache.shenyu.k8s.common.GatewayApiConstants; +import org.apache.shenyu.k8s.common.IngressConfiguration; +import org.apache.shenyu.k8s.common.JsonFields; +import org.apache.shenyu.k8s.common.ListenerSupport; +import org.apache.shenyu.k8s.common.ReferenceGrants; +import org.apache.shenyu.k8s.common.ShenyuMemoryConfig; +import org.apache.shenyu.k8s.common.StatusMergePatch; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +/** + * Reconciler for HTTPRoute resources (Gateway API v1). + * + * <p>Every parentRef is evaluated individually and reported in status.parents — including + * rejections (no ReferenceGrant, listener policy, hostname mismatch, missing parent) with + * the spec-defined reason — except parentRefs resolved to Gateways owned by another + * controller, whose status entries belong to that controller. + */ +public class HTTPRouteReconciler implements Reconciler { + + private static final Logger LOG = LoggerFactory.getLogger(HTTPRouteReconciler.class); + + private final Lister<DynamicKubernetesObject> httpRouteLister; + + private final Lister<DynamicKubernetesObject> gatewayLister; + + private final Lister<DynamicKubernetesObject> gatewayClassLister; + + private final Lister<DynamicKubernetesObject> referenceGrantLister; + + private final HttpRouteParser httpRouteParser; + + private final ShenyuCacheRepository shenyuCacheRepository; + + private final ApiClient apiClient; + + /** The port the embedded ShenYu data plane actually listens on ({@code server.port}); route attachment is allowed only on listeners the gateway really serves. */ + private final int servedPort; + + public HTTPRouteReconciler(final SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer, + final SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient, + final int servedPort) { + this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer()); + this.gatewayLister = new Lister<>(gatewayInformer.getIndexer()); + this.gatewayClassLister = new Lister<>(gatewayClassInformer.getIndexer()); + this.referenceGrantLister = new Lister<>(referenceGrantInformer.getIndexer()); + this.httpRouteParser = httpRouteParser; + this.shenyuCacheRepository = shenyuCacheRepository; + this.apiClient = apiClient; + this.servedPort = servedPort; + } + + @Override + public Result reconcile(final Request request) { + String namespace = request.getNamespace(); + String routeName = request.getName(); + LOG.debug("Starting to reconcile HTTPRoute {}/{}", namespace, routeName); + + DynamicKubernetesObject httpRoute = httpRouteLister.namespace(namespace).get(routeName); + if (Objects.isNull(httpRoute)) { + deleteConfig(namespace, routeName); + return new Result(false); + } + + List<ParentDecision> decisions = evaluateParents(httpRoute); + if (decisions.isEmpty()) { + // Not attached to any ShenYu-managed Gateway; drop previously programmed config + // and our status entries, then leave the route to other controllers. + deleteConfig(namespace, routeName); + removeShenyuParentStatus(httpRoute, namespace, routeName); + return new Result(false); + } + + List<ParentDecision> accepted = new ArrayList<>(); + for (ParentDecision decision : decisions) { + if (decision.accepted) { + accepted.add(decision); + } + } + + ShenyuMemoryConfig config = null; + if (accepted.isEmpty()) { + // All ShenYu parents rejected the attachment: clean up but still report status. + deleteConfig(namespace, routeName); + } else { + config = httpRouteParser.parse(httpRoute, effectiveHostnames(accepted)); + if (config.isHasUnsupportedFilters()) { + deleteConfig(namespace, routeName); + } else { + GatewayRouteCache cache = GatewayRouteCache.getInstance(); + 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()); + applyConfig(config, namespace, routeName); + deleteStaleSelectors(namespace, routeName, oldSelectorIds, newSelectorIds); + cache.putRouteSelectors(namespace, routeName, PluginEnum.DIVIDE.getName(), newSelectorIds); + rebindGateways(cache, namespace, routeName, accepted); + } + } + + updateHTTPRouteStatus(httpRoute, decisions, config, namespace, routeName); + return new Result(false); + } + + private List<String> selectorIdsOf(final ShenyuMemoryConfig config) { + List<String> ids = new ArrayList<>(); + for (IngressConfiguration rc : config.getRouteConfigList()) { + ids.add(rc.getSelectorData().getId()); + } + return ids; + } + + /** + * Effective hostnames for the data plane: the union of the per-parent intersections of + * route hostnames with listener hostnames. Empty means "any host" — and one accepting + * parent with an empty hostname list makes the route host-agnostic overall, so the + * union with other parents' hostnames must not narrow it back down. + */ + private List<String> effectiveHostnames(final List<ParentDecision> accepted) { + Set<String> union = new LinkedHashSet<>(); + for (ParentDecision decision : accepted) { + if (decision.hostnames.isEmpty()) { + return List.of(); + } + union.addAll(decision.hostnames); + } + return new ArrayList<>(union); + } + + /** + * Rebuild the route→gateway bindings from the currently accepted parents at listener + * granularity, dropping stale bindings of parentRefs that were removed or became + * ineligible. The per-listener bindings drive the Gateway listeners' + * attachedRoutes counts. + */ + private void rebindGateways(final GatewayRouteCache cache, final String namespace, final String routeName, + final List<ParentDecision> accepted) { + cache.removeRouteGatewayBinding(namespace, routeName); + for (ParentDecision decision : accepted) { + cache.bindRouteToGateway(decision.parentNamespace, decision.parentName, decision.listenerNames, + namespace, routeName); + } + } + + /** + * Evaluate every parentRef of the route. Only verdicts this controller is responsible + * for are returned: parentRefs of a foreign kind or group, and parentRefs resolving to + * a Gateway owned by another controller, are skipped silently. + */ + private List<ParentDecision> evaluateParents(final DynamicKubernetesObject httpRoute) { + String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace(); + JsonObject spec = JsonFields.getJsonObject(httpRoute.getRaw(), "spec"); + JsonArray parentRefs = JsonFields.getJsonArray(spec, "parentRefs"); + List<String> routeHostnames = readStringList(JsonFields.getJsonArray(spec, "hostnames")); + List<ParentDecision> decisions = new ArrayList<>(); + if (Objects.isNull(parentRefs)) { + return decisions; + } + for (JsonElement element : parentRefs) { + if (!element.isJsonObject()) { + continue; + } + JsonObject parentRef = element.getAsJsonObject(); + String group = JsonFields.getString(parentRef, "group"); + String kind = JsonFields.getString(parentRef, "kind"); + if ((Objects.nonNull(group) && !GatewayApiConstants.GATEWAY_API_GROUP.equals(group)) + || (Objects.nonNull(kind) && !GatewayApiConstants.GATEWAY_KIND.equals(kind))) { + continue; + } + String parentName = JsonFields.getString(parentRef, "name"); + if (Objects.isNull(parentName)) { + continue; + } + String parentNamespace = JsonFields.getString(parentRef, "namespace"); + if (Objects.isNull(parentNamespace)) { + parentNamespace = routeNamespace; + } + DynamicKubernetesObject gateway = gatewayLister.namespace(parentNamespace).get(parentName); + if (Objects.isNull(gateway)) { + decisions.add(ParentDecision.rejected(parentRef, parentNamespace, parentName, + GatewayApiConstants.REASON_NO_MATCHING_PARENT, + "Gateway " + parentNamespace + "/" + parentName + " does not exist")); + continue; + } + if (!isShenyuGateway(gateway)) { + continue; + } + if (!parentNamespace.equals(routeNamespace) + && !ReferenceGrants.isGranted(referenceGrantLister, parentNamespace, routeNamespace, + GatewayApiConstants.GATEWAY_API_GROUP, GatewayApiConstants.GATEWAY_KIND, parentName)) { + decisions.add(ParentDecision.rejected(parentRef, parentNamespace, parentName, + GatewayApiConstants.REASON_REF_NOT_PERMITTED, + "cross-namespace parent reference to Gateway " + parentNamespace + "/" + parentName + + " is not permitted by any ReferenceGrant")); + continue; + } Review Comment: Cross-namespace HTTPRoute→Gateway attachment is authorized by the listener's `allowedRoutes`; ReferenceGrant does not apply to a Route's parentRef. This currently rejects a valid route even when the listener allows its namespace unless an unrelated grant exists. Remove this grant check and let `evaluateListeners` enforce `allowedRoutes`; retain ReferenceGrant validation for cross-namespace backendRefs. ########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.k8s; + +import io.kubernetes.client.extended.controller.Controller; +import io.kubernetes.client.extended.controller.ControllerManager; +import io.kubernetes.client.extended.controller.DefaultController; +import io.kubernetes.client.extended.controller.builder.ControllerBuilder; +import io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.workqueue.DefaultRateLimitingQueue; +import io.kubernetes.client.extended.workqueue.RateLimitingQueue; +import io.kubernetes.client.extended.workqueue.WorkQueue; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.enums.PluginRoleEnum; +import org.apache.shenyu.k8s.cache.K8sCacheReadiness; +import org.apache.shenyu.k8s.common.GatewayApiConstants; +import org.apache.shenyu.k8s.common.GatewayApiCrdVerifier; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler; +import org.apache.shenyu.k8s.reconciler.GatewayReconciler; +import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler; +import org.apache.shenyu.k8s.reconciler.HttpRouteEndpointsHandler; +import org.apache.shenyu.k8s.reconciler.ReferenceGrantReconciler; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber; +import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber; +import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Spring Boot auto-configuration for the Kubernetes Gateway API controller mode. + * + * <p>The ShenYu bootstrap embeds the Kubernetes controller and serves as both control plane + * and data plane in a single JVM: the controller watches Gateway API resources and writes + * the parsed selector/rule config directly into the in-process {@code BaseDataCache}. + * + * <p><b>Multiple replicas supported</b> without leader election: reconciliation is idempotent + * (selector/rule IDs are derived deterministically from the route coordinates, so every + * replica converges to the same cache) and status patches are skipped when unchanged. + * Deployments MUST use a readiness probe gated on the {@code k8sCacheReadiness} health + * indicator so a cold pod receives no traffic before its informers finish the initial sync. + */ +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * Informer resync period. Periodic resync re-drives changes not covered by watches + * (e.g. a ReferenceGrant added after a route reported ResolvedRefs=False) and retries + * failed status patches. Set here because ControllerWatch's withResyncPeriod is a + * no-op in client-java. + */ + private static final long RESYNC_PERIOD_MILLIS = Duration.ofMinutes(1).toMillis(); + + /** Fallback for server.port; matches the bootstrap default. */ + private static final int DEFAULT_SERVER_PORT = 9195; + + /** + * GatewayClass informer factory; separate factories avoid DynamicKubernetesObject class-key collisions. + * + * @param apiClient the Kubernetes API client + * @return the GatewayClass SharedInformerFactory + */ + @Bean("gatewayclass-shared-informer-factory") + public SharedInformerFactory gatewayClassSharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "gatewayclasses", + apiClient); + factory.sharedIndexInformerFor(gatewayClassApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + return factory; + } + + @Bean("gateway-shared-informer-factory") + public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "gateways", + apiClient); + factory.sharedIndexInformerFor(gatewayApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + return factory; + } + + /** + * HTTPRoute, Service and Endpoints informer factory. Services are watched to map a + * backendRef's Service port to its targetPort (including named targetPorts), which the + * Endpoints alone cannot express for multi-port Services. + * + * @param apiClient the Kubernetes API client + * @return the HTTPRoute, Service and Endpoints SharedInformerFactory + */ + @Bean("httproute-shared-informer-factory") + public SharedInformerFactory httpRouteSharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "httproutes", + apiClient); + factory.sharedIndexInformerFor(httpRouteApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + + GenericKubernetesApi<V1Service, V1ServiceList> serviceApi = new GenericKubernetesApi<>(V1Service.class, + V1ServiceList.class, "", "v1", "services", apiClient); + factory.sharedIndexInformerFor(serviceApi, V1Service.class, RESYNC_PERIOD_MILLIS); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, RESYNC_PERIOD_MILLIS); + return factory; + } + + /** + * ReferenceGrant informer factory. Grants live in the namespace of the referenced + * resource; the informer is consumed read-only for cross-namespace validation. + * + * @param apiClient the Kubernetes API client + * @return the ReferenceGrant SharedInformerFactory + */ + @Bean("referencegrant-shared-informer-factory") + public SharedInformerFactory referenceGrantSharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi referenceGrantApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "referencegrants", + apiClient); + factory.sharedIndexInformerFor(referenceGrantApi, DynamicKubernetesObject.class, 0); + return factory; + } + + /** + * Shared executor for all controller managers, with graceful shutdown on context close. + * + * @return daemon cached thread pool executor + */ + @Bean(destroyMethod = "shutdown") + public ExecutorService controllerExecutorService() { + return Executors.newCachedThreadPool(r -> { + Thread t = new Thread(r, "shenyu-k8s-controller"); + t.setDaemon(true); + return t; + }); + } + + @Bean("gatewayclass-controller-manager") + public ControllerManager gatewayClassControllerManager( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController) { + return new ControllerManager(gatewayClassFactory, gatewayClassController); + } + + @Bean("gateway-controller-manager") + public ControllerManager gatewayControllerManager( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gateway-controller") final Controller gatewayController) { + return new ControllerManager(gatewayFactory, gatewayController); + } + + @Bean("httproute-controller-manager") + @DependsOn("httpRouteEndpointsHandler") + public ControllerManager httpRouteControllerManager( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-controller") final Controller httpRouteController) { + return new ControllerManager(httpRouteFactory, httpRouteController); + } + + /** + * ReferenceGrant controller: re-queues HTTPRoutes referencing the grant's namespace, so + * grant changes take effect immediately instead of on the next route resync (a revoked + * grant must stop unauthorized traffic right away). + * + * @param referenceGrantFactory the ReferenceGrant SharedInformerFactory + * @param httpRouteFactory the HTTPRoute SharedInformerFactory + * @param httpRouteWorkQueue the HTTPRoute controller work queue + * @return the ReferenceGrant controller + */ + @Bean("referencegrant-controller") + public Controller referenceGrantController( + @Qualifier("referencegrant-shared-informer-factory") final SharedInformerFactory referenceGrantFactory, + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-work-queue") final RateLimitingQueue<Request> httpRouteWorkQueue) { + DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(referenceGrantFactory); + builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q) + .build()); + builder.withWorkerCount(1); + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + Reconciler reconciler = new ReferenceGrantReconciler(httpRouteInformer, httpRouteWorkQueue); + return builder.withReconciler(reconciler).withName("referenceGrantController").build(); + } + + @Bean("referencegrant-controller-manager") + public ControllerManager referenceGrantControllerManager( + @Qualifier("referencegrant-shared-informer-factory") final SharedInformerFactory referenceGrantFactory, + @Qualifier("referencegrant-controller") final Controller referenceGrantController) { + return new ControllerManager(referenceGrantFactory, referenceGrantController); + } + + /** + * Fail fast when the cluster does not serve the required Gateway API CRDs at v1, + * before any informer starts crash-looping on 404s. Runs after all singletons are + * instantiated and before the controller lifecycle starts the informers. + * + * @param apiClient the Kubernetes API client + * @return the startup check + */ + @Bean + public SmartInitializingSingleton gatewayApiCrdVerifier(final ApiClient apiClient) { + return () -> GatewayApiCrdVerifier.verify(apiClient); + } + + /** + * Start all controller managers after context refresh (and stop them on close); + * see {@link ControllerManagerLifecycle}. + * + * @param controllerManagers all controller managers of this mode + * @param controllerExecutorService the shared controller executor + * @return the lifecycle driving the controllers + */ + @Bean + public SmartLifecycle k8sControllerLifecycle(final List<ControllerManager> controllerManagers, + final ExecutorService controllerExecutorService) { + return new ControllerManagerLifecycle(controllerManagers, controllerExecutorService); + } + + @Bean("gatewayclass-controller") + public Controller gatewayClassController( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + final GatewayClassReconciler gatewayClassReconciler) { + DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(gatewayClassFactory); + builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q) + .build()); + builder.withWorkerCount(1); + return builder.withReconciler(gatewayClassReconciler).withName("gatewayClassController").build(); + } + + @Bean("gateway-controller") + public Controller gatewayController( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + final GatewayReconciler gatewayReconciler) { + DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(gatewayFactory); + builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q) + .build()); + builder.withWorkerCount(2); + return builder.withReconciler(gatewayReconciler).withName("gatewayController").build(); + } + + /** + * Shared work queue for the HTTPRoute controller, also fed by the Endpoints handler. + * + * @param controllerExecutorService the shared controller executor + * @return the HTTPRoute controller work queue + */ + @Bean("httproute-work-queue") + public RateLimitingQueue<Request> httpRouteWorkQueue(final ExecutorService controllerExecutorService) { + return new DefaultRateLimitingQueue<>(controllerExecutorService); + } + + @Bean("httproute-controller") + public Controller httpRouteController( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + final HTTPRouteReconciler httpRouteReconciler, + @Qualifier("httproute-work-queue") final RateLimitingQueue<Request> httpRouteWorkQueue) { + DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(httpRouteFactory) + .withWorkQueue(httpRouteWorkQueue); + builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q) + .build()); + builder.withWorkerCount(2); + return builder.withReconciler(httpRouteReconciler).withName("httpRouteController").build(); + } + + /** + * Enqueues HTTPRoutes whose backendRefs target a changed Service. Declared as a + * dependency of the HTTPRoute controller manager so its indexers are registered + * before the informers start. + * + * @param httpRouteFactory the HTTPRoute and Endpoints SharedInformerFactory + * @param httpRouteWorkQueue the HTTPRoute controller work queue + * @return the registered Endpoints event handler + */ + @Bean + public HttpRouteEndpointsHandler httpRouteEndpointsHandler( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-work-queue") final RateLimitingQueue<Request> httpRouteWorkQueue) { + SharedIndexInformer<V1Endpoints> endpointsInformer = + httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class); + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + HttpRouteEndpointsHandler handler = new HttpRouteEndpointsHandler(httpRouteInformer, httpRouteWorkQueue); + endpointsInformer.addEventHandler(handler); + return handler; + } + + @Bean + public GatewayClassReconciler gatewayClassReconciler( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gateway-controller") final Controller gatewayController, + final ApiClient apiClient) { + SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer = + gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> gatewayInformer = + gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + RateLimitingQueue<Request> gatewayWorkQueue = ((DefaultController) gatewayController).getWorkQueue(); + return new GatewayClassReconciler(gatewayClassInformer, gatewayInformer, gatewayWorkQueue, apiClient); + } + + @Bean + public GatewayReconciler gatewayReconciler( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-controller") final Controller httpRouteController, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient, + final Environment environment) { + SharedIndexInformer<DynamicKubernetesObject> gatewayInformer = + gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer = + gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + RateLimitingQueue<Request> httpRouteWorkQueue = ((DefaultController) httpRouteController).getWorkQueue(); + int servedPort = environment.getProperty("server.port", Integer.class, DEFAULT_SERVER_PORT); + return new GatewayReconciler(gatewayInformer, gatewayClassInformer, httpRouteInformer, + shenyuCacheRepository, httpRouteWorkQueue, apiClient, servedPort); + } + + @Bean + public HTTPRouteReconciler httpRouteReconciler( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("referencegrant-shared-informer-factory") final SharedInformerFactory referenceGrantFactory, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient, + final Environment environment) { + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> gatewayInformer = + gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer = + gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer = + referenceGrantFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + int servedPort = environment.getProperty("server.port", Integer.class, DEFAULT_SERVER_PORT); + return new HTTPRouteReconciler(httpRouteInformer, gatewayInformer, gatewayClassInformer, + referenceGrantInformer, httpRouteParser, shenyuCacheRepository, apiClient, servedPort); + } + + @Bean + public HttpRouteParser httpRouteParser( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("referencegrant-shared-informer-factory") final SharedInformerFactory referenceGrantFactory) { + SharedIndexInformer<V1Service> serviceInformer = + httpRouteFactory.getExistingSharedIndexInformer(V1Service.class); + SharedIndexInformer<V1Endpoints> endpointsInformer = + httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class); + SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer = + referenceGrantFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + Lister<V1Service> serviceLister = new Lister<>(serviceInformer.getIndexer()); + Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsInformer.getIndexer()); + Lister<DynamicKubernetesObject> referenceGrantLister = new Lister<>(referenceGrantInformer.getIndexer()); + return new HttpRouteParser(endpointsLister, serviceLister, referenceGrantLister); + } + + @Bean + public ShenyuCacheRepository shenyuCacheRepository(final CommonPluginDataSubscriber pluginDataSubscriber, + final CommonDiscoveryUpstreamDataSubscriber discoveryUpstreamDataSubscriber, + final MetaDataCacheSubscriber metaDataSubscriber, + final MetaDataCacheSubscriber metaDataCacheSubscriber) { + ShenyuCacheRepository repository = new ShenyuCacheRepository(pluginDataSubscriber, discoveryUpstreamDataSubscriber, metaDataSubscriber, metaDataCacheSubscriber); + enablePlugin(repository, PluginEnum.GLOBAL, null); + enablePlugin(repository, PluginEnum.URI, null); + enablePlugin(repository, PluginEnum.NETTY_HTTP_CLIENT, null); + enablePlugin(repository, PluginEnum.DIVIDE, "{multiSelectorHandle: 1, multiRuleHandle:0}"); + enablePlugin(repository, PluginEnum.GENERAL_CONTEXT, null); + return repository; + } + + /** + * Readiness aggregator over all registered informers and the controller work queues of + * this mode: informer sync alone does not mean the objects were reconciled into the + * local cache yet, so the queues' initial backlog must drain too. + * + * @param gatewayClassFactory the GatewayClass SharedInformerFactory + * @param gatewayFactory the Gateway SharedInformerFactory + * @param httpRouteFactory the HTTPRoute, Service and Endpoints SharedInformerFactory + * @param referenceGrantFactory the ReferenceGrant SharedInformerFactory + * @param gatewayClassController the GatewayClass controller (for its work queue) + * @param gatewayController the Gateway controller (for its work queue) + * @param httpRouteWorkQueue the HTTPRoute controller work queue + * @return readiness aggregator over all registered informers and work queues + */ + @Bean + public K8sCacheReadiness k8sCacheReadiness( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("referencegrant-shared-informer-factory") final SharedInformerFactory referenceGrantFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController, + @Qualifier("gateway-controller") final Controller gatewayController, + @Qualifier("httproute-work-queue") final RateLimitingQueue<Request> httpRouteWorkQueue) { + List<SharedIndexInformer<?>> informers = new ArrayList<>(); + informers.add(gatewayClassFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class)); + informers.add(gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class)); + informers.add(httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class)); + informers.add(httpRouteFactory.getExistingSharedIndexInformer(V1Service.class)); + informers.add(httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class)); + informers.add(referenceGrantFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class)); + if (informers.stream().anyMatch(Objects::isNull)) { + throw new IllegalStateException("Expected informer not registered; informer factory wiring is inconsistent"); + } + List<WorkQueue<?>> workQueues = List.of( + ((DefaultController) gatewayClassController).getWorkQueue(), + ((DefaultController) gatewayController).getWorkQueue(), + httpRouteWorkQueue); Review Comment: The ReferenceGrant controller's work queue is omitted from readiness. After informer sync, readiness can latch while initial grants are still pending, so HTTPRoutes reconciled earlier may remain denied and unprogrammed until that controller eventually requeues them. Include the ReferenceGrant queue—and account for its in-flight reconcile—before declaring the cache ready. ########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,513 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.shenyu.springboot.starter.k8s; + +import io.kubernetes.client.extended.controller.Controller; +import io.kubernetes.client.extended.controller.ControllerManager; +import io.kubernetes.client.extended.controller.DefaultController; +import io.kubernetes.client.extended.controller.builder.ControllerBuilder; +import io.kubernetes.client.extended.controller.builder.DefaultControllerBuilder; +import io.kubernetes.client.extended.controller.reconciler.Request; +import io.kubernetes.client.extended.controller.reconciler.Reconciler; +import io.kubernetes.client.extended.workqueue.DefaultRateLimitingQueue; +import io.kubernetes.client.extended.workqueue.RateLimitingQueue; +import io.kubernetes.client.extended.workqueue.WorkQueue; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.SharedInformerFactory; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.openapi.models.V1EndpointsList; +import io.kubernetes.client.openapi.models.V1Service; +import io.kubernetes.client.openapi.models.V1ServiceList; +import io.kubernetes.client.util.generic.GenericKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesApi; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.enums.PluginRoleEnum; +import org.apache.shenyu.k8s.cache.K8sCacheReadiness; +import org.apache.shenyu.k8s.common.GatewayApiConstants; +import org.apache.shenyu.k8s.common.GatewayApiCrdVerifier; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.reconciler.GatewayClassReconciler; +import org.apache.shenyu.k8s.reconciler.GatewayReconciler; +import org.apache.shenyu.k8s.reconciler.HTTPRouteReconciler; +import org.apache.shenyu.k8s.reconciler.HttpRouteEndpointsHandler; +import org.apache.shenyu.k8s.reconciler.ReferenceGrantReconciler; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.apache.shenyu.plugin.base.cache.CommonDiscoveryUpstreamDataSubscriber; +import org.apache.shenyu.plugin.base.cache.CommonPluginDataSubscriber; +import org.apache.shenyu.plugin.global.subsciber.MetaDataCacheSubscriber; +import org.springframework.beans.factory.SmartInitializingSingleton; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.SmartLifecycle; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.core.env.Environment; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Spring Boot auto-configuration for the Kubernetes Gateway API controller mode. + * + * <p>The ShenYu bootstrap embeds the Kubernetes controller and serves as both control plane + * and data plane in a single JVM: the controller watches Gateway API resources and writes + * the parsed selector/rule config directly into the in-process {@code BaseDataCache}. + * + * <p><b>Multiple replicas supported</b> without leader election: reconciliation is idempotent + * (selector/rule IDs are derived deterministically from the route coordinates, so every + * replica converges to the same cache) and status patches are skipped when unchanged. + * Deployments MUST use a readiness probe gated on the {@code k8sCacheReadiness} health + * indicator so a cold pod receives no traffic before its informers finish the initial sync. + */ +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * Informer resync period. Periodic resync re-drives changes not covered by watches + * (e.g. a ReferenceGrant added after a route reported ResolvedRefs=False) and retries + * failed status patches. Set here because ControllerWatch's withResyncPeriod is a + * no-op in client-java. + */ + private static final long RESYNC_PERIOD_MILLIS = Duration.ofMinutes(1).toMillis(); + + /** Fallback for server.port; matches the bootstrap default. */ + private static final int DEFAULT_SERVER_PORT = 9195; + + /** + * GatewayClass informer factory; separate factories avoid DynamicKubernetesObject class-key collisions. + * + * @param apiClient the Kubernetes API client + * @return the GatewayClass SharedInformerFactory + */ + @Bean("gatewayclass-shared-informer-factory") + public SharedInformerFactory gatewayClassSharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi gatewayClassApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "gatewayclasses", + apiClient); + factory.sharedIndexInformerFor(gatewayClassApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + return factory; + } + + @Bean("gateway-shared-informer-factory") + public SharedInformerFactory gatewaySharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi gatewayApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "gateways", + apiClient); + factory.sharedIndexInformerFor(gatewayApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + return factory; + } + + /** + * HTTPRoute, Service and Endpoints informer factory. Services are watched to map a + * backendRef's Service port to its targetPort (including named targetPorts), which the + * Endpoints alone cannot express for multi-port Services. + * + * @param apiClient the Kubernetes API client + * @return the HTTPRoute, Service and Endpoints SharedInformerFactory + */ + @Bean("httproute-shared-informer-factory") + public SharedInformerFactory httpRouteSharedInformerFactory(final ApiClient apiClient) { + SharedInformerFactory factory = new SharedInformerFactory(apiClient); + DynamicKubernetesApi httpRouteApi = new DynamicKubernetesApi( + GatewayApiConstants.GATEWAY_API_GROUP, + GatewayApiConstants.GATEWAY_API_VERSION, + "httproutes", + apiClient); + factory.sharedIndexInformerFor(httpRouteApi, DynamicKubernetesObject.class, RESYNC_PERIOD_MILLIS); + + GenericKubernetesApi<V1Service, V1ServiceList> serviceApi = new GenericKubernetesApi<>(V1Service.class, + V1ServiceList.class, "", "v1", "services", apiClient); + factory.sharedIndexInformerFor(serviceApi, V1Service.class, RESYNC_PERIOD_MILLIS); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, RESYNC_PERIOD_MILLIS); Review Comment: The Service informer is populated but has no event handler. Changing a Service port or `targetPort` does not update Endpoints, so routes keep the old pod port until the periodic HTTPRoute resync; an initial route reconcile can likewise use the fallback before the Service cache is ready. Register Service add/update/delete events to enqueue routes through the existing backend-service index. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
