Copilot commented on code in PR #6347: URL: https://github.com/apache/shenyu/pull/6347#discussion_r3272479044
########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,261 @@ +/* + * 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.workqueue.RateLimitingQueue; +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.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.common.GatewayApiConstants; +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.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.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.concurrent.Executors; + +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * GatewayClass SharedInformerFactory - only registers GatewayClass informer. + * Separate factory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for GatewayClass resources + */ + @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, 0); + return factory; + } + + /** + * Gateway SharedInformerFactory - only registers Gateway informer. + * Separate from other factories to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for Gateway resources + */ + @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, 0); + return factory; + } + + /** + * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints informers. + * Separate from gatewayFactory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for HTTPRoute and Endpoints resources + */ + @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, 0); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0); + return factory; + } + + @Bean("gatewayclass-controller-manager") + public ControllerManager gatewayClassControllerManager( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController) { + ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; Review Comment: These ControllerManager beans start a new single-thread executor but never shut it down (and each `@Bean` call creates a new executor). This can leak threads and also prevent clean Spring context shutdown. Consider injecting a shared ExecutorService bean with a destroyMethod (shutdown) or using a Spring TaskExecutor / Lifecycle-aware component to manage the controller threads. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java: ########## @@ -0,0 +1,304 @@ +/* + * 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.parser; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.models.V1EndpointAddress; +import io.kubernetes.client.openapi.models.V1EndpointSubset; +import io.kubernetes.client.openapi.models.V1Endpoints; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.dto.ConditionData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +import org.apache.shenyu.common.dto.convert.rule.impl.DivideRuleHandle; +import org.apache.shenyu.common.dto.convert.selector.DivideUpstream; +import org.apache.shenyu.common.enums.LoadBalanceEnum; +import org.apache.shenyu.common.enums.MatchModeEnum; +import org.apache.shenyu.common.enums.OperatorEnum; +import org.apache.shenyu.common.enums.ParamTypeEnum; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.enums.SelectorTypeEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.apache.shenyu.k8s.cache.GatewayRouteCache; +import org.apache.shenyu.k8s.common.IngressConfiguration; +import org.apache.shenyu.k8s.common.ShenyuMemoryConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class HttpRouteParser { + + private static final Logger LOG = LoggerFactory.getLogger(HttpRouteParser.class); + + private final Lister<V1Endpoints> endpointsLister; + + public HttpRouteParser(final Lister<V1Endpoints> endpointsLister) { + this.endpointsLister = endpointsLister; + } + + public ShenyuMemoryConfig parse(final DynamicKubernetesObject httpRoute) { + ShenyuMemoryConfig res = new ShenyuMemoryConfig(); + String namespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace(); + String routeName = httpRoute.getMetadata().getName(); + + JsonObject raw = httpRoute.getRaw(); + JsonObject spec = raw.getAsJsonObject("spec"); + if (Objects.isNull(spec)) { + return res; + } + + JsonArray hostnames = spec.getAsJsonArray("hostnames"); + JsonArray rules = spec.getAsJsonArray("rules"); + if (Objects.isNull(rules) || rules.isEmpty()) { + return res; + } + + GatewayRouteCache cache = GatewayRouteCache.getInstance(); + List<IngressConfiguration> routeConfigList = new ArrayList<>(); + + for (int ruleIndex = 0; ruleIndex < rules.size(); ruleIndex++) { + processRule(rules.get(ruleIndex).getAsJsonObject(), hostnames, namespace, routeName, ruleIndex, cache, routeConfigList); + } + + res.setRouteConfigList(routeConfigList); + return res; + } + + private void processRule(final JsonObject rule, final JsonArray hostnames, final String namespace, + final String routeName, final int ruleIndex, final GatewayRouteCache cache, + final List<IngressConfiguration> routeConfigList) { + JsonArray backendRefs = rule.getAsJsonArray("backendRefs"); + if (Objects.isNull(backendRefs) || backendRefs.isEmpty()) { + return; + } + + List<DivideUpstream> upstreamList = parseBackendRefs(backendRefs, namespace); + + // Build a list of individual hostname conditions. + // Each hostname generates a separate selector+rule to avoid AND logic contradiction + // (a request can only match one hostname at a time). + List<ConditionData> hostnameConditions = new ArrayList<>(); + if (Objects.nonNull(hostnames) && !hostnames.isEmpty()) { + for (JsonElement hostname : hostnames) { + ConditionData hostCondition = new ConditionData(); + hostCondition.setParamType(ParamTypeEnum.DOMAIN.getName()); + hostCondition.setOperator(OperatorEnum.EQ.getAlias()); + hostCondition.setParamValue(hostname.getAsString()); + hostnameConditions.add(hostCondition); + } + } + + JsonArray matches = rule.getAsJsonArray("matches"); + if (Objects.nonNull(matches) && !matches.isEmpty()) { + for (JsonElement matchElement : matches) { + JsonObject match = matchElement.getAsJsonObject(); + List<ConditionData> matchConditions = new ArrayList<>(); + appendMatchConditions(matchConditions, match); + + if (hostnameConditions.isEmpty()) { + // No hostname: one selector+rule for this match + String selectorId = cache.generateSelectorId(); + String selectorName = routeName + "-rule-" + ruleIndex; + SelectorData selectorData = buildSelectorData(selectorId, selectorName, matchConditions, upstreamList); + RuleData ruleData = buildRuleData(cache.generateRuleId(), selectorId, selectorName, matchConditions); + cache.addRouteSelector(namespace, routeName, PluginEnum.DIVIDE.getName(), selectorId); + routeConfigList.add(new IngressConfiguration(selectorData, List.of(ruleData), null)); + } else { + // One selector+rule per hostname to keep AND semantics correct + for (ConditionData hostCondition : hostnameConditions) { + List<ConditionData> conditions = new ArrayList<>(); + conditions.add(hostCondition); + conditions.addAll(matchConditions); + + String selectorId = cache.generateSelectorId(); + String selectorName = routeName + "-rule-" + ruleIndex; + SelectorData selectorData = buildSelectorData(selectorId, selectorName, conditions, upstreamList); + RuleData ruleData = buildRuleData(cache.generateRuleId(), selectorId, selectorName, conditions); + cache.addRouteSelector(namespace, routeName, PluginEnum.DIVIDE.getName(), selectorId); + routeConfigList.add(new IngressConfiguration(selectorData, List.of(ruleData), null)); Review Comment: When hostnames are present, multiple selectors/rules are generated but selectorName is always routeName + "-rule-" + ruleIndex. This produces duplicate selector/rule names for different hostnames/matches, which makes logs/debugging and any name-based tooling harder. Consider including the hostname and/or match index in the generated names to keep them distinct. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,358 @@ +/* + * 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.Gson; +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.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; +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.ShenyuMemoryConfig; +import org.apache.shenyu.k8s.parser.HttpRouteParser; +import org.apache.shenyu.k8s.repository.ShenyuCacheRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +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 HttpRouteParser httpRouteParser; + + private final ShenyuCacheRepository shenyuCacheRepository; + + private final ApiClient apiClient; + + public HTTPRouteReconciler(final SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer, + final SharedIndexInformer<DynamicKubernetesObject> gatewayInformer, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient) { + this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer()); + this.gatewayLister = new Lister<>(gatewayInformer.getIndexer()); + this.httpRouteParser = httpRouteParser; + this.shenyuCacheRepository = shenyuCacheRepository; + this.apiClient = apiClient; + } + + @Override + public Result reconcile(final Request request) { + LOG.info("Starting to reconcile HTTPRoute {}", request); + try { + String namespace = request.getNamespace(); + String routeName = request.getName(); + DynamicKubernetesObject httpRoute = httpRouteLister.namespace(namespace).get(routeName); + + if (Objects.isNull(httpRoute)) { + deleteConfig(namespace, routeName); + return new Result(false); + } + + if (!isBoundToShenyuGateway(httpRoute)) { + LOG.info("HTTPRoute {} is not bound to a ShenYu Gateway, skipping", request); + return new Result(false); + } + + deleteConfig(namespace, routeName); + + ShenyuMemoryConfig config = httpRouteParser.parse(httpRoute); + applyConfig(config); + + bindToGateway(httpRoute); + + updateHTTPRouteStatus(httpRoute); + + LOG.info("HTTPRoute {} reconciled successfully", request); + return new Result(false); + } catch (Exception e) { + LOG.error("Error reconciling HTTPRoute {}, will retry", request, e); + return new Result(true); + } + } + + private boolean isBoundToShenyuGateway(final DynamicKubernetesObject httpRoute) { + JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec"); + if (Objects.isNull(spec) || !spec.has("parentRefs")) { + return false; + } + JsonArray parentRefs = spec.getAsJsonArray("parentRefs"); + if (Objects.isNull(parentRefs)) { + return false; + } + String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace(); + for (JsonElement element : parentRefs) { + JsonObject parentRef = element.getAsJsonObject(); + String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null; + String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace; + String sectionName = parentRef.has("sectionName") ? parentRef.get("sectionName").getAsString() : null; + if (Objects.isNull(parentName)) { + continue; + } + DynamicKubernetesObject gateway = gatewayLister.namespace(parentNamespace).get(parentName); + if (Objects.nonNull(gateway) && GatewayReconciler.isShenyuGateway(gateway)) { + // If sectionName is specified, verify the Gateway has a matching listener + if (Objects.nonNull(sectionName) && !hasMatchingListener(gateway, sectionName)) { + LOG.info("HTTPRoute references sectionName '{}' but Gateway {}/{} has no matching listener", sectionName, parentNamespace, parentName); + continue; + } + return true; + } + } + return false; + } + + private boolean hasMatchingListener(final DynamicKubernetesObject gateway, final String sectionName) { + JsonObject spec = gateway.getRaw().getAsJsonObject("spec"); + if (Objects.isNull(spec) || !spec.has("listeners")) { + return false; + } + JsonArray listeners = spec.getAsJsonArray("listeners"); + if (Objects.isNull(listeners)) { + return false; + } + for (JsonElement listenerElement : listeners) { + JsonObject listener = listenerElement.getAsJsonObject(); + if (listener.has("name") && sectionName.equals(listener.get("name").getAsString())) { + return true; + } + } + return false; + } + + private void deleteConfig(final String namespace, final String routeName) { + GatewayRouteCache cache = GatewayRouteCache.getInstance(); + List<String> selectorIds = cache.removeRouteSelectors(namespace, routeName, PluginEnum.DIVIDE.getName()); + if (CollectionUtils.isNotEmpty(selectorIds)) { + for (String selectorId : selectorIds) { + List<RuleData> rules = shenyuCacheRepository.findRuleDataList(selectorId); + if (CollectionUtils.isNotEmpty(rules)) { + for (RuleData rule : new ArrayList<>(rules)) { + shenyuCacheRepository.deleteRuleData(PluginEnum.DIVIDE.getName(), selectorId, rule.getId()); + } + } + shenyuCacheRepository.deleteSelectorData(PluginEnum.DIVIDE.getName(), selectorId); + } + } + cache.removeRouteGatewayBinding(namespace, routeName); + } + + private void applyConfig(final ShenyuMemoryConfig config) { + List<IngressConfiguration> routeConfigs = config.getRouteConfigList(); + if (CollectionUtils.isEmpty(routeConfigs)) { + return; + } + for (IngressConfiguration routeConfig : routeConfigs) { + SelectorData selectorData = routeConfig.getSelectorData(); + shenyuCacheRepository.saveOrUpdateSelectorData(selectorData); + for (RuleData ruleData : routeConfig.getRuleDataList()) { + shenyuCacheRepository.saveOrUpdateRuleData(ruleData); + } + } + } + + private void bindToGateway(final DynamicKubernetesObject httpRoute) { + JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec"); + if (Objects.isNull(spec) || !spec.has("parentRefs")) { + return; + } + JsonArray parentRefs = spec.getAsJsonArray("parentRefs"); + String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace(); + String routeName = httpRoute.getMetadata().getName(); + + for (JsonElement element : parentRefs) { + JsonObject parentRef = element.getAsJsonObject(); + String parentName = parentRef.has("name") ? parentRef.get("name").getAsString() : null; + String parentNamespace = parentRef.has("namespace") ? parentRef.get("namespace").getAsString() : routeNamespace; + if (Objects.nonNull(parentName)) { + GatewayRouteCache.getInstance().bindRouteToGateway(parentNamespace, parentName, routeNamespace, routeName); + } + } + } + + /** + * Update HTTPRoute status with Accepted=True and ResolvedRefs=True for each ShenYu-managed parent. + * Uses merge-patch on the /status subresource, same approach as GatewayReconciler. + * Skips the patch if the status is already up-to-date to avoid triggering an infinite reconcile loop. + */ + private void updateHTTPRouteStatus(final DynamicKubernetesObject httpRoute) { + if (isRouteStatusAlreadySet(httpRoute)) { + return; + } + try { + final String routeNamespace = Objects.requireNonNull(httpRoute.getMetadata()).getNamespace(); + final String routeName = httpRoute.getMetadata().getName(); + + JsonObject spec = httpRoute.getRaw().getAsJsonObject("spec"); + if (Objects.isNull(spec) || !spec.has("parentRefs")) { + return; + } + JsonArray parentRefs = spec.getAsJsonArray("parentRefs"); + JsonArray parentsStatus = buildParentsStatus(parentRefs, routeNamespace); + + if (parentsStatus.size() == 0) { + return; + } + sendStatusPatch(routeNamespace, routeName, parentsStatus); + } catch (Exception e) { + LOG.warn("Failed to update HTTPRoute status, will retry on next resync", e); + } + } + + /** + * Check if the HTTPRoute already has Accepted=True condition from the ShenYu controller + * in its status.parents, to avoid unnecessary status patches that trigger infinite reconcile loops. + */ + private boolean isRouteStatusAlreadySet(final DynamicKubernetesObject httpRoute) { + JsonObject raw = httpRoute.getRaw(); + if (!raw.has("status") || raw.get("status").isJsonNull()) { + return false; + } + JsonObject status = raw.getAsJsonObject("status"); + if (!status.has("parents") || status.get("parents").isJsonNull()) { + return false; + } + JsonArray parents = status.getAsJsonArray("parents"); + for (JsonElement parentElement : parents) { + JsonObject parent = parentElement.getAsJsonObject(); + if (!parent.has("controllerName") || !GatewayApiConstants.SHENYU_CONTROLLER_NAME.equals(parent.get("controllerName").getAsString())) { + continue; + } + if (!parent.has("conditions") || parent.get("conditions").isJsonNull()) { + continue; + } + JsonArray conditions = parent.getAsJsonArray("conditions"); + for (JsonElement condElement : conditions) { + JsonObject cond = condElement.getAsJsonObject(); + if ("Accepted".equals(cond.has("type") ? cond.get("type").getAsString() : null) + && "True".equals(cond.has("status") ? cond.get("status").getAsString() : null)) { + return true; + } Review Comment: isRouteStatusAlreadySet() only checks for an Accepted=True condition, but updateHTTPRouteStatus() documents setting both Accepted=True and ResolvedRefs=True and intends to skip patching only when status is up-to-date. As written, a route with Accepted=True but missing/False ResolvedRefs will never be patched again. Consider verifying both conditions (and/or checking the exact parentRef entry) before skipping the status patch. ########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,261 @@ +/* + * 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.workqueue.RateLimitingQueue; +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.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.common.GatewayApiConstants; +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.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.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.concurrent.Executors; + +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * GatewayClass SharedInformerFactory - only registers GatewayClass informer. + * Separate factory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for GatewayClass resources + */ + @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, 0); + return factory; + } + + /** + * Gateway SharedInformerFactory - only registers Gateway informer. + * Separate from other factories to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for Gateway resources + */ + @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, 0); + return factory; + } + + /** + * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints informers. + * Separate from gatewayFactory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for HTTPRoute and Endpoints resources + */ + @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, 0); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0); + return factory; + } + + @Bean("gatewayclass-controller-manager") + public ControllerManager gatewayClassControllerManager( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController) { + ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @Bean("gateway-controller-manager") + public ControllerManager gatewayControllerManager( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gateway-controller") final Controller gatewayController) { + ControllerManager controllerManager = new ControllerManager(gatewayFactory, gatewayController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @Bean("httproute-controller-manager") + public ControllerManager httpRouteControllerManager( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-controller") final Controller httpRouteController) { + ControllerManager controllerManager = new ControllerManager(httpRouteFactory, httpRouteController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @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) + .withResyncPeriod(Duration.ofMinutes(1)) + .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) + .withResyncPeriod(Duration.ofMinutes(1)) + .build()); + builder.withWorkerCount(2); + return builder.withReconciler(gatewayReconciler).withName("gatewayController").build(); + } + + @Bean("httproute-controller") + public Controller httpRouteController( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + final HTTPRouteReconciler httpRouteReconciler) { + DefaultControllerBuilder builder = ControllerBuilder.defaultBuilder(httpRouteFactory); + builder = builder.watch(q -> ControllerBuilder.controllerWatchBuilder(DynamicKubernetesObject.class, q) + .withResyncPeriod(Duration.ofMinutes(1)) + .build()); + builder.withWorkerCount(2); + return builder.withReconciler(httpRouteReconciler).withName("httpRouteController").build(); + } + + @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("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-controller") final Controller httpRouteController, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient) { + SharedIndexInformer<DynamicKubernetesObject> gatewayInformer = + gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + RateLimitingQueue<Request> httpRouteWorkQueue = ((DefaultController) httpRouteController).getWorkQueue(); + return new GatewayReconciler(gatewayInformer, httpRouteInformer, shenyuCacheRepository, httpRouteWorkQueue, apiClient); + } + + @Bean + public HTTPRouteReconciler httpRouteReconciler( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + final HttpRouteParser httpRouteParser, + final ShenyuCacheRepository shenyuCacheRepository, + final ApiClient apiClient) { + SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer = + httpRouteFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + SharedIndexInformer<DynamicKubernetesObject> gatewayInformer = + gatewayFactory.getExistingSharedIndexInformer(DynamicKubernetesObject.class); + return new HTTPRouteReconciler(httpRouteInformer, gatewayInformer, httpRouteParser, shenyuCacheRepository, apiClient); + } + + @Bean + public HttpRouteParser httpRouteParser( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory) { + SharedIndexInformer<V1Endpoints> endpointsInformer = + httpRouteFactory.getExistingSharedIndexInformer(V1Endpoints.class); + Lister<V1Endpoints> endpointsLister = new Lister<>(endpointsInformer.getIndexer()); + return new HttpRouteParser(endpointsLister); + } + + @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); Review Comment: The ShenyuCacheRepository bean method takes two MetaDataCacheSubscriber parameters. With a single MetaDataCacheSubscriber bean on the classpath, Spring will inject the same instance twice, and ShenyuCacheRepository will call onSubscribe/unSubscribe twice for metadata operations. Prefer wiring a MetaDataSubscriber for the first parameter (or otherwise dedupe in ShenyuCacheRepository) to avoid duplicate work and to allow non-cache MetaDataSubscriber implementations when present. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java: ########## @@ -0,0 +1,271 @@ +/* + * 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.Gson; +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.extended.workqueue.RateLimitingQueue; +import io.kubernetes.client.informer.SharedIndexInformer; +import io.kubernetes.client.informer.cache.Lister; +import io.kubernetes.client.openapi.ApiClient; +import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shenyu.common.dto.RuleData; +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.repository.ShenyuCacheRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class GatewayReconciler implements Reconciler { + + private static final Logger LOG = LoggerFactory.getLogger(GatewayReconciler.class); + + private final Lister<DynamicKubernetesObject> gatewayLister; + + private final Lister<DynamicKubernetesObject> httpRouteLister; + + private final ShenyuCacheRepository shenyuCacheRepository; + + private final RateLimitingQueue<Request> httpRouteWorkQueue; + + private final ApiClient apiClient; + + public GatewayReconciler(final SharedIndexInformer<DynamicKubernetesObject> gatewayInformer, + final SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer, + final ShenyuCacheRepository shenyuCacheRepository, + final RateLimitingQueue<Request> httpRouteWorkQueue, + final ApiClient apiClient) { + this.gatewayLister = new Lister<>(gatewayInformer.getIndexer()); + this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer()); + this.shenyuCacheRepository = shenyuCacheRepository; + this.httpRouteWorkQueue = httpRouteWorkQueue; + this.apiClient = apiClient; + } + + @Override + public Result reconcile(final Request request) { + LOG.info("Starting to reconcile gateway {}", request); + try { + DynamicKubernetesObject gateway = gatewayLister.namespace(request.getNamespace()).get(request.getName()); + + if (Objects.isNull(gateway)) { + LOG.info("Gateway {} deleted, cleaning associated routes", request); + deleteAssociatedRoutes(request.getNamespace(), request.getName()); + return new Result(false); + } + + if (!isShenyuGateway(gateway)) { + LOG.info("Gateway {} is not managed by ShenYu, skipping", request); + return new Result(false); + } + + updateGatewayAcceptedStatus(gateway); + + // Re-queue HTTPRoutes that reference this Gateway but haven't been applied yet + requeueAffectedHTTPRoutes(request.getNamespace(), request.getName()); + + LOG.info("Gateway {} reconciled successfully", request); + return new Result(false); + } catch (Exception e) { + LOG.error("Error reconciling gateway {}, will retry", request, e); + return new Result(true); + } + } + + /** + * When a ShenYu Gateway is created/updated, find HTTPRoutes whose parentRefs reference + * this Gateway and add them to the HTTPRoute controller's work queue for re-reconciliation. + * This handles the case where an HTTPRoute was created before the Gateway existed. + * Also handles cross-namespace references where HTTPRoute's parentRef specifies a different namespace. + */ + private void requeueAffectedHTTPRoutes(final String gatewayNamespace, final String gatewayName) { + // Search in the gateway's namespace (same-namespace reference) + List<DynamicKubernetesObject> localRoutes = httpRouteLister.namespace(gatewayNamespace).list(); + for (DynamicKubernetesObject route : localRoutes) { + if (isBoundToGateway(route, gatewayNamespace, gatewayName)) { + Request req = new Request(route.getMetadata().getNamespace(), route.getMetadata().getName()); + httpRouteWorkQueue.add(req); + LOG.info("Re-queued HTTPRoute {}/{} due to Gateway {}/{} reconciliation", + route.getMetadata().getNamespace(), route.getMetadata().getName(), + gatewayNamespace, gatewayName); + } + } + // Also search all namespaces for cross-namespace references + for (DynamicKubernetesObject route : httpRouteLister.list()) { + String routeNamespace = Objects.requireNonNull(route.getMetadata()).getNamespace(); + if (routeNamespace.equals(gatewayNamespace)) { + // Already handled in local routes search above + continue; + } + if (isBoundToGateway(route, gatewayNamespace, gatewayName)) { + Request req = new Request(route.getMetadata().getNamespace(), route.getMetadata().getName()); + httpRouteWorkQueue.add(req); + LOG.info("Re-queued cross-namespace HTTPRoute {}/{} due to Gateway {}/{} reconciliation", + route.getMetadata().getNamespace(), route.getMetadata().getName(), + gatewayNamespace, gatewayName); + } + } Review Comment: requeueAffectedHTTPRoutes() iterates over all HTTPRoutes cluster-wide on every Gateway reconcile (httpRouteLister.list()), which can be expensive in large clusters and may create unnecessary queue churn. Consider using an index/cached binding (e.g., GatewayRouteCache) to target only routes that reference the Gateway, or maintaining an index keyed by parentRef to avoid full scans. ########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,261 @@ +/* + * 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.workqueue.RateLimitingQueue; +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.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.common.GatewayApiConstants; +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.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.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.concurrent.Executors; + +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * GatewayClass SharedInformerFactory - only registers GatewayClass informer. + * Separate factory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for GatewayClass resources + */ + @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, 0); + return factory; + } + + /** + * Gateway SharedInformerFactory - only registers Gateway informer. + * Separate from other factories to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for Gateway resources + */ + @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, 0); + return factory; + } + + /** + * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints informers. + * Separate from gatewayFactory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for HTTPRoute and Endpoints resources + */ + @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, 0); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0); + return factory; + } + + @Bean("gatewayclass-controller-manager") + public ControllerManager gatewayClassControllerManager( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController) { + ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @Bean("gateway-controller-manager") + public ControllerManager gatewayControllerManager( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gateway-controller") final Controller gatewayController) { + ControllerManager controllerManager = new ControllerManager(gatewayFactory, gatewayController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @Bean("httproute-controller-manager") + public ControllerManager httpRouteControllerManager( + @Qualifier("httproute-shared-informer-factory") final SharedInformerFactory httpRouteFactory, + @Qualifier("httproute-controller") final Controller httpRouteController) { + ControllerManager controllerManager = new ControllerManager(httpRouteFactory, httpRouteController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; Review Comment: This ControllerManager bean starts a new single-thread executor but never shuts it down. Consider reusing a shared ExecutorService bean with a destroyMethod (shutdown) or making the controller manager lifecycle-managed to avoid thread leaks and improve graceful shutdown. ########## shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/GatewayApiControllerConfiguration.java: ########## @@ -0,0 +1,261 @@ +/* + * 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.workqueue.RateLimitingQueue; +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.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.common.GatewayApiConstants; +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.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.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; +import java.util.concurrent.Executors; + +@Configuration +@ConditionalOnProperty(name = "shenyu.k8s.mode", havingValue = "gateway-api") +public class GatewayApiControllerConfiguration { + + /** + * GatewayClass SharedInformerFactory - only registers GatewayClass informer. + * Separate factory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for GatewayClass resources + */ + @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, 0); + return factory; + } + + /** + * Gateway SharedInformerFactory - only registers Gateway informer. + * Separate from other factories to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for Gateway resources + */ + @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, 0); + return factory; + } + + /** + * HTTPRoute SharedInformerFactory - registers HTTPRoute and Endpoints informers. + * Separate from gatewayFactory to avoid DynamicKubernetesObject class key collision. + * + * @param apiClient the Kubernetes API client + * @return the SharedInformerFactory for HTTPRoute and Endpoints resources + */ + @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, 0); + + GenericKubernetesApi<V1Endpoints, V1EndpointsList> endpointsApi = new GenericKubernetesApi<>(V1Endpoints.class, + V1EndpointsList.class, "", "v1", "endpoints", apiClient); + factory.sharedIndexInformerFor(endpointsApi, V1Endpoints.class, 0); + return factory; + } + + @Bean("gatewayclass-controller-manager") + public ControllerManager gatewayClassControllerManager( + @Qualifier("gatewayclass-shared-informer-factory") final SharedInformerFactory gatewayClassFactory, + @Qualifier("gatewayclass-controller") final Controller gatewayClassController) { + ControllerManager controllerManager = new ControllerManager(gatewayClassFactory, gatewayClassController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; + } + + @Bean("gateway-controller-manager") + public ControllerManager gatewayControllerManager( + @Qualifier("gateway-shared-informer-factory") final SharedInformerFactory gatewayFactory, + @Qualifier("gateway-controller") final Controller gatewayController) { + ControllerManager controllerManager = new ControllerManager(gatewayFactory, gatewayController); + Executors.newSingleThreadExecutor().submit(controllerManager); + return controllerManager; Review Comment: This ControllerManager bean starts a new single-thread executor but never shuts it down. Consider reusing a shared ExecutorService bean with a destroyMethod (shutdown) or making the controller manager lifecycle-managed to avoid thread leaks and improve graceful shutdown. -- 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]
