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]


Reply via email to