Copilot commented on code in PR #6347: URL: https://github.com/apache/shenyu/pull/6347#discussion_r3370617454
########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/GatewayRouteCache.java: ########## @@ -0,0 +1,131 @@ +/* + * 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.cache; + +import com.google.common.collect.Maps; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +public final class GatewayRouteCache { + + private static final GatewayRouteCache INSTANCE = new GatewayRouteCache(); + + private static final Map<String, List<String>> ROUTE_SELECTOR_MAP = Maps.newConcurrentMap(); + + private static final Map<String, List<String>> GATEWAY_ROUTE_MAP = Maps.newConcurrentMap(); + + private static final Map<String, String> ROUTE_GATEWAY_MAP = Maps.newConcurrentMap(); + + private static final AtomicLong GROWING_ID = new AtomicLong(10000); + + private GatewayRouteCache() { + } + + public static GatewayRouteCache getInstance() { + return INSTANCE; + } + + public void putRouteSelectors(final String namespace, final String routeName, + final String pluginName, final List<String> selectorIds) { + ROUTE_SELECTOR_MAP.put(routeKey(namespace, routeName, pluginName), selectorIds); + } + + public void addRouteSelector(final String namespace, final String routeName, + final String pluginName, final String selectorId) { + ROUTE_SELECTOR_MAP.computeIfAbsent(routeKey(namespace, routeName, pluginName), + k -> new CopyOnWriteArrayList<>()).add(selectorId); + } + + public List<String> getRouteSelectors(final String namespace, final String routeName, + final String pluginName) { + return ROUTE_SELECTOR_MAP.get(routeKey(namespace, routeName, pluginName)); + } + + public List<String> removeRouteSelectors(final String namespace, final String routeName, + final String pluginName) { + return ROUTE_SELECTOR_MAP.remove(routeKey(namespace, routeName, pluginName)); + } + + public void bindRouteToGateway(final String gatewayNamespace, final String gatewayName, + final String routeNamespace, final String routeName) { + String gwKey = gatewayKey(gatewayNamespace, gatewayName); + GATEWAY_ROUTE_MAP.computeIfAbsent(gwKey, k -> new CopyOnWriteArrayList<>()).add(routeKey(routeNamespace, routeName)); + ROUTE_GATEWAY_MAP.put(routeKey(routeNamespace, routeName), gwKey); Review Comment: bindRouteToGateway() appends the route key into GATEWAY_ROUTE_MAP on every reconcile without deduplication. Over time this can accumulate duplicate entries for the same route (especially with periodic resyncs), causing repeated re-queues and unbounded memory growth in the cache. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/GatewayRouteCache.java: ########## @@ -0,0 +1,131 @@ +/* + * 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.cache; + +import com.google.common.collect.Maps; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicLong; + +public final class GatewayRouteCache { + + private static final GatewayRouteCache INSTANCE = new GatewayRouteCache(); + + private static final Map<String, List<String>> ROUTE_SELECTOR_MAP = Maps.newConcurrentMap(); + + private static final Map<String, List<String>> GATEWAY_ROUTE_MAP = Maps.newConcurrentMap(); + + private static final Map<String, String> ROUTE_GATEWAY_MAP = Maps.newConcurrentMap(); + + private static final AtomicLong GROWING_ID = new AtomicLong(10000); + + private GatewayRouteCache() { + } + + public static GatewayRouteCache getInstance() { + return INSTANCE; + } + + public void putRouteSelectors(final String namespace, final String routeName, + final String pluginName, final List<String> selectorIds) { + ROUTE_SELECTOR_MAP.put(routeKey(namespace, routeName, pluginName), selectorIds); + } + + public void addRouteSelector(final String namespace, final String routeName, + final String pluginName, final String selectorId) { + ROUTE_SELECTOR_MAP.computeIfAbsent(routeKey(namespace, routeName, pluginName), + k -> new CopyOnWriteArrayList<>()).add(selectorId); + } + + public List<String> getRouteSelectors(final String namespace, final String routeName, + final String pluginName) { + return ROUTE_SELECTOR_MAP.get(routeKey(namespace, routeName, pluginName)); + } + + public List<String> removeRouteSelectors(final String namespace, final String routeName, + final String pluginName) { + return ROUTE_SELECTOR_MAP.remove(routeKey(namespace, routeName, pluginName)); + } + + public void bindRouteToGateway(final String gatewayNamespace, final String gatewayName, + final String routeNamespace, final String routeName) { + String gwKey = gatewayKey(gatewayNamespace, gatewayName); + GATEWAY_ROUTE_MAP.computeIfAbsent(gwKey, k -> new CopyOnWriteArrayList<>()).add(routeKey(routeNamespace, routeName)); + ROUTE_GATEWAY_MAP.put(routeKey(routeNamespace, routeName), gwKey); + } + + public List<String> getRoutesByGateway(final String gatewayNamespace, final String gatewayName) { + return GATEWAY_ROUTE_MAP.get(gatewayKey(gatewayNamespace, gatewayName)); + } + + public List<String> removeRoutesByGateway(final String gatewayNamespace, final String gatewayName) { + String gwKey = gatewayKey(gatewayNamespace, gatewayName); + List<String> routes = GATEWAY_ROUTE_MAP.remove(gwKey); + if (Objects.nonNull(routes)) { + routes.forEach(ROUTE_GATEWAY_MAP::remove); + } + return routes; + } + + public String getGatewayForRoute(final String routeNamespace, final String routeName) { + return ROUTE_GATEWAY_MAP.get(routeKey(routeNamespace, routeName)); + } + + public void removeRouteGatewayBinding(final String routeNamespace, final String routeName) { + String routeKey = routeKey(routeNamespace, routeName); + String gwKey = ROUTE_GATEWAY_MAP.remove(routeKey); + if (Objects.nonNull(gwKey)) { + List<String> routes = GATEWAY_ROUTE_MAP.get(gwKey); + if (Objects.nonNull(routes)) { + routes.remove(routeKey); + } Review Comment: removeRouteGatewayBinding() only removes a single occurrence of the route key from the gateway's route list. If duplicates exist (e.g., from repeated bindRouteToGateway calls), stale entries will remain and continue to trigger re-queues / deletions later. ########## shenyu-examples/shenyu-examples-http/k8s/gateway-api.yml: ########## @@ -0,0 +1,55 @@ +# 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. + +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: shenyu +spec: + controllerName: gateway.shenyu.apache.org/shenyu-controller +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: shenyu-gateway + annotations: + shenyu.apache.org/loadbalancer: p2c +spec: Review Comment: gateway-api.yml sets the Gateway annotation `shenyu.apache.org/loadbalancer: p2c`, but the new Gateway API controller/parser code does not read Gateway annotations to configure load-balancing (HTTPRoute parser currently hard-codes RANDOM). This annotation is therefore misleading in the example as written. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,371 @@ +/* + * 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 both Accepted=True and ResolvedRefs=True conditions + * from the ShenYu controller in its status.parents, to avoid unnecessary status patches + * that trigger infinite reconcile loops. + * Both conditions must be present because updateHTTPRouteStatus() always sets both together; + * checking only Accepted=True would leave routes with a partial status never repaired. + */ + 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"); + boolean hasAccepted = false; + boolean hasResolvedRefs = false; + for (JsonElement condElement : conditions) { + JsonObject cond = condElement.getAsJsonObject(); + String type = cond.has("type") ? cond.get("type").getAsString() : null; + String condStatus = cond.has("status") ? cond.get("status").getAsString() : null; + if ("True".equals(condStatus)) { + if ("Accepted".equals(type)) { + hasAccepted = true; + } else if ("ResolvedRefs".equals(type)) { + hasResolvedRefs = true; + } + } + } + if (hasAccepted && hasResolvedRefs) { + return true; + } + } + return false; Review Comment: isRouteStatusAlreadySet() returns true as soon as it finds *any* ShenYu-owned parent entry with Accepted=True and ResolvedRefs=True. If there are multiple ShenYu-managed parentRefs and one of them is missing/partial, this early return will prevent updateHTTPRouteStatus() from repairing the incomplete status, leaving the resource out-of-date. ########## shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java: ########## @@ -0,0 +1,371 @@ +/* + * 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); Review Comment: updateHTTPRouteStatus() builds a new status.parents array containing only ShenYu-managed parents and then applies it via JSON merge-patch. Because merge-patch replaces arrays wholesale, this will drop any existing status.parents entries written by other controllers (or other ShenYu parent entries not included), which is not desirable for Gateway API status interoperability. -- 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]
