Copilot commented on code in PR #6347:
URL: https://github.com/apache/shenyu/pull/6347#discussion_r3901137784


##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/common/GatewayApiConstants.java:
##########
@@ -0,0 +1,128 @@
+/*
+ * 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.common;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+
+import java.util.Objects;
+
+public final class GatewayApiConstants {
+
+    public static final String GATEWAY_API_GROUP = "gateway.networking.k8s.io";
+
+    public static final String GATEWAY_API_VERSION = "v1";
+
+    public static final String GATEWAY_KIND = "Gateway";
+
+    public static final String HTTP_ROUTE_KIND = "HTTPRoute";
+
+    public static final String SERVICE_KIND = "Service";
+
+    /** The core (legacy) API group, named by the empty string; group of 
Service. */
+    public static final String CORE_API_GROUP = "";
+
+    /** ResolvedRefs=False reason: a backendRef's Service has no ready 
endpoints. */
+    public static final String REASON_BACKEND_NOT_FOUND = "BackendNotFound";
+
+    /** ResolvedRefs=False / Accepted=False reason: a cross-namespace 
reference (parentRef or backendRef) is not permitted by a ReferenceGrant or 
listener policy. */
+    public static final String REASON_REF_NOT_PERMITTED = "RefNotPermitted";
+
+    /** ResolvedRefs=False reason: a backendRef's kind is not Service, the 
only supported kind. */
+    public static final String REASON_INVALID_KIND = "InvalidKind";
+
+    /** Condition type Accepted. */
+    public static final String CONDITION_ACCEPTED = "Accepted";
+
+    /** Condition type ResolvedRefs. */
+    public static final String CONDITION_RESOLVED_REFS = "ResolvedRefs";
+
+    /** Condition type Programmed. */
+    public static final String CONDITION_PROGRAMMED = "Programmed";
+
+    /** Accepted=False reason: parentRef does not resolve to an existing 
Gateway. */
+    public static final String REASON_NO_MATCHING_PARENT = "NoMatchingParent";
+
+    /** Accepted=False reason: no listener hostname intersects the route 
hostnames. */
+    public static final String REASON_NO_MATCHING_LISTENER_HOSTNAME = 
"NoMatchingListenerHostname";
+
+    /** Condition reason: a spec value (filter, sectionName, ...) is 
unsupported. */
+    public static final String REASON_UNSUPPORTED_VALUE = "UnsupportedValue";
+
+    /** Listener Accepted=False reason: protocol is not supported (only HTTP 
is served). */
+    public static final String REASON_UNSUPPORTED_PROTOCOL = 
"UnsupportedProtocol";
+
+    /** Listener Accepted=False reason: port is not served by this gateway. */
+    public static final String REASON_PORT_UNAVAILABLE = "PortUnavailable";
+
+    /** Condition reason: programmed into the data plane. */
+    public static final String REASON_PROGRAMMED = "Programmed";
+
+    /** Gateway Programmed=False reason: no listener with a supported protocol 
and port. */
+    public static final String REASON_LISTENERS_NOT_VALID = 
"ListenersNotValid";
+
+    /** The only listener protocol this controller serves. */
+    public static final String PROTOCOL_HTTP = "HTTP";
+
+    public static final String SHENYU_CONTROLLER_NAME = 
"gateway.shenyu.apache.org/shenyu-controller";
+
+    private GatewayApiConstants() {
+    }
+
+    /**
+     * Whether a backendRef references a Service: {@code kind} defaults to 
Service when
+     * absent, the only kind ShenYu can resolve to upstream addresses.
+     *
+     * @param backendRef the backendRef object
+     * @return true if the reference targets a Service
+     */
+    public static boolean isServiceRef(final JsonObject backendRef) {
+        String kind = JsonFields.getString(backendRef, "kind");
+        return Objects.isNull(kind) || SERVICE_KIND.equals(kind);
+    }

Review Comment:
   This treats any object whose kind is `Service` as a core Kubernetes Service, 
even when `group` explicitly names another API group. Such a backendRef can 
therefore be resolved to an unrelated core Service with the same name instead 
of being reported as `InvalidKind`. Require the group to be absent or empty as 
well.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayClassReconciler.java:
##########
@@ -0,0 +1,238 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.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.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.JsonFields;
+import org.apache.shenyu.k8s.common.StatusMergePatch;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * Reconciler for the cluster-scoped GatewayClass resources: accepts classes 
whose
+ * spec.controllerName matches ShenYu's controller name (Accepted=True 
status), and on
+ * deletion re-queues the Gateways referencing the class for cascade cleanup.
+ */
+public class GatewayClassReconciler implements Reconciler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GatewayClassReconciler.class);
+
+    private static final String GATEWAY_CLASS_KIND = "GatewayClass";
+
+    private static final String GATEWAYCLASSES_RESOURCE = "gatewayclasses";
+
+    private final Lister<DynamicKubernetesObject> gatewayClassLister;
+
+    private final Lister<DynamicKubernetesObject> gatewayLister;
+
+    private final RateLimitingQueue<Request> gatewayWorkQueue;
+
+    private final ApiClient apiClient;
+
+    public GatewayClassReconciler(final 
SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer,
+                                  final 
SharedIndexInformer<DynamicKubernetesObject> gatewayInformer,
+                                  final RateLimitingQueue<Request> 
gatewayWorkQueue,
+                                  final ApiClient apiClient) {
+        this.gatewayClassLister = new 
Lister<>(gatewayClassInformer.getIndexer());
+        this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+        this.gatewayWorkQueue = gatewayWorkQueue;
+        this.apiClient = apiClient;
+    }
+
+    @Override
+    public Result reconcile(final Request request) {
+        LOG.info("Starting to reconcile GatewayClass {}", request.getName());
+        try {
+            DynamicKubernetesObject gatewayClass = 
gatewayClassLister.get(request.getName());
+
+            if (Objects.isNull(gatewayClass)) {
+                LOG.info("GatewayClass {} deleted, re-queuing affected 
Gateways", request.getName());
+                requeueAffectedGateways(request.getName());
+                return new Result(false);
+            }
+
+            if (!isShenyuGatewayClass(gatewayClass)) {
+                LOG.info("GatewayClass {} is not managed by ShenYu, skipping", 
request.getName());
+                return new Result(false);

Review Comment:
   When an existing GatewayClass changes `controllerName` away from ShenYu, 
this branch skips it without requeuing Gateways that previously used it. Their 
routes and Accepted status remain active until unrelated/resync events. Requeue 
Gateways referencing the class on this ownership-loss transition so cleanup 
happens immediately.
   
   This issue also appears on line 170 of the same file.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/cache/K8sCacheReadiness.java:
##########
@@ -0,0 +1,59 @@
+/*
+ * 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 io.kubernetes.client.informer.SharedIndexInformer;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Aggregates the initial-sync state of all informers into a single readiness 
signal, used
+ * to gate Kubernetes readiness so a cold pod (empty {@code BaseDataCache}) 
receives no
+ * traffic until its local cache holds the full cluster state.
+ *
+ * <p>Readiness latches: once every informer completed its initial LIST, it 
reports ready
+ * forever. During a transient API server outage the local cache still serves 
the last
+ * known state, so flapping back to not-ready would only cause needless 
endpoint churn.
+ */
+public final class K8sCacheReadiness {
+
+    private final List<SharedIndexInformer<?>> informers;
+
+    private volatile boolean ready;
+
+    public K8sCacheReadiness(final Collection<SharedIndexInformer<?>> 
informers) {
+        if (Objects.isNull(informers) || informers.isEmpty()) {
+            throw new IllegalArgumentException("At least one informer is 
required");
+        }
+        this.informers = List.copyOf(informers);
+    }
+
+    public boolean isReady() {
+        if (ready) {
+            return true;
+        }
+        ready = informers.stream().allMatch(SharedIndexInformer::hasSynced);
+        return ready;

Review Comment:
   `hasSynced()` only means each informer's initial LIST populated its index; 
it does not mean the controller work queues have reconciled those objects into 
`BaseDataCache`. On a large cluster this can mark the pod ready while routes 
are still absent, contrary to the readiness guarantee above. Gate readiness on 
completion of the initial reconciliation backlog (in addition to informer sync).



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/repository/ShenyuCacheRepository.java:
##########
@@ -114,22 +118,81 @@ public List<SelectorData> findSelectorDataList(final 
String pluginName) {
     }
 
     /**
-     * Save or update SelectorData by SelectorData.
+     * Save or update SelectorData by SelectorData. Idempotent: a selector 
whose cached
+     * content is already equal (including the upstream list in its handle) is 
skipped, so
+     * the periodic informer resync does not churn the data plane with no-op 
updates.
      *
      * @param selectorData SelectorData
      */
     public void saveOrUpdateSelectorData(final SelectorData selectorData) {
-        subscriber.onSelectorSubscribe(selectorData);
+        List<DiscoveryUpstreamData> upstreamDataList = new 
ArrayList<>(convert(selectorData.getPluginName(), selectorData.getHandle()));
+        Set<String> newUrls = 
upstreamDataList.stream().map(DiscoveryUpstreamData::getUrl).collect(Collectors.toSet());
+        List<Upstream> cachedUpstreams = 
UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selectorData.getId());
+        Set<String> cachedUrls = CollectionUtils.isEmpty(cachedUpstreams) ? 
Collections.emptySet()
+                : 
cachedUpstreams.stream().map(Upstream::getUrl).collect(Collectors.toSet());

Review Comment:
   Upstream identity is `(protocol, URL)` in `UpstreamCacheManager`, but these 
snapshots compare URL alone and the stale-removal loop also tests only URL 
membership. Changing an existing address from `http://` to `https://` submits 
the new upstream without offlining the old protocol entry, leaving both 
routable. Build and compare identity keys from both protocol and URL.



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/HTTPRouteReconciler.java:
##########
@@ -0,0 +1,675 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.informer.SharedIndexInformer;
+import io.kubernetes.client.informer.cache.Lister;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.ApiException;
+import io.kubernetes.client.util.generic.dynamic.DynamicKubernetesObject;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.JsonFields;
+import org.apache.shenyu.k8s.common.ListenerSupport;
+import org.apache.shenyu.k8s.common.ReferenceGrants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.common.StatusMergePatch;
+import org.apache.shenyu.k8s.parser.HttpRouteParser;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Reconciler for HTTPRoute resources (Gateway API v1).
+ *
+ * <p>Every parentRef is evaluated individually and reported in status.parents 
— including
+ * rejections (no ReferenceGrant, listener policy, hostname mismatch, missing 
parent) with
+ * the spec-defined reason — except parentRefs resolved to Gateways owned by 
another
+ * controller, whose status entries belong to that controller.
+ */
+public class HTTPRouteReconciler implements Reconciler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(HTTPRouteReconciler.class);
+
+    private final Lister<DynamicKubernetesObject> httpRouteLister;
+
+    private final Lister<DynamicKubernetesObject> gatewayLister;
+
+    private final Lister<DynamicKubernetesObject> gatewayClassLister;
+
+    private final Lister<DynamicKubernetesObject> referenceGrantLister;
+
+    private final HttpRouteParser httpRouteParser;
+
+    private final ShenyuCacheRepository shenyuCacheRepository;
+
+    private final ApiClient apiClient;
+
+    public HTTPRouteReconciler(final 
SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer,
+                               final 
SharedIndexInformer<DynamicKubernetesObject> gatewayInformer,
+                               final 
SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer,
+                               final 
SharedIndexInformer<DynamicKubernetesObject> referenceGrantInformer,
+                               final HttpRouteParser httpRouteParser,
+                               final ShenyuCacheRepository 
shenyuCacheRepository,
+                               final ApiClient apiClient) {
+        this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+        this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+        this.gatewayClassLister = new 
Lister<>(gatewayClassInformer.getIndexer());
+        this.referenceGrantLister = new 
Lister<>(referenceGrantInformer.getIndexer());
+        this.httpRouteParser = httpRouteParser;
+        this.shenyuCacheRepository = shenyuCacheRepository;
+        this.apiClient = apiClient;
+    }
+
+    @Override
+    public Result reconcile(final Request request) {
+        String namespace = request.getNamespace();
+        String routeName = request.getName();
+        LOG.debug("Starting to reconcile HTTPRoute {}/{}", namespace, 
routeName);
+
+        DynamicKubernetesObject httpRoute = 
httpRouteLister.namespace(namespace).get(routeName);
+        if (Objects.isNull(httpRoute)) {
+            deleteConfig(namespace, routeName);
+            return new Result(false);
+        }
+
+        List<ParentDecision> decisions = evaluateParents(httpRoute);
+        if (decisions.isEmpty()) {
+            // Not attached to any ShenYu-managed Gateway; drop previously 
programmed config
+            // and our status entries, then leave the route to other 
controllers.
+            deleteConfig(namespace, routeName);
+            removeShenyuParentStatus(httpRoute, namespace, routeName);
+            return new Result(false);
+        }
+
+        List<ParentDecision> accepted = new ArrayList<>();
+        for (ParentDecision decision : decisions) {
+            if (decision.accepted) {
+                accepted.add(decision);
+            }
+        }
+
+        ShenyuMemoryConfig config = null;
+        if (accepted.isEmpty()) {
+            // All ShenYu parents rejected the attachment: clean up but still 
report status.
+            deleteConfig(namespace, routeName);
+        } else {
+            config = httpRouteParser.parse(httpRoute, 
effectiveHostnames(accepted));
+            if (config.isHasUnsupportedFilters()) {
+                // Accepted=False must not coexist with programmed traffic: 
the route would
+                // only be partially applied, so nothing is programmed at all 
and any config
+                // from a previous (valid) spec of this route is dropped.
+                deleteConfig(namespace, routeName);
+            } else {
+                GatewayRouteCache cache = GatewayRouteCache.getInstance();
+                List<String> newSelectorIds = selectorIdsOf(config);
+                // read the previous snapshot before putRouteSelectors 
overwrites it
+                List<String> oldSelectorIds = Objects.requireNonNullElse(
+                        cache.getRouteSelectors(namespace, routeName, 
PluginEnum.DIVIDE.getName()), List.of());
+                cache.putRouteSelectors(namespace, routeName, 
PluginEnum.DIVIDE.getName(), newSelectorIds);
+                deleteStaleSelectors(namespace, routeName, oldSelectorIds, 
newSelectorIds);
+                applyConfig(config, namespace, routeName);
+                rebindGateways(cache, namespace, routeName, accepted);

Review Comment:
   Stale selectors are deleted before their replacements are published. 
Deterministic IDs are stable only for unchanged coordinates; edits such as 
adding a hostname produce new IDs, so this ordering creates a live interval 
with no matching route. Apply the new snapshot first, then remove stale 
selectors, and only commit the cache's new ID snapshot after those operations 
succeed.
   
   This issue also appears in the following locations of the same file:
   - line 162
   - line 234
   - line 262
   - line 509



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/GatewayReconciler.java:
##########
@@ -0,0 +1,454 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.shenyu.k8s.reconciler;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.kubernetes.client.extended.controller.reconciler.Reconciler;
+import io.kubernetes.client.extended.controller.reconciler.Request;
+import io.kubernetes.client.extended.controller.reconciler.Result;
+import io.kubernetes.client.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.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.GatewayRouteCache;
+import org.apache.shenyu.k8s.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.JsonFields;
+import org.apache.shenyu.k8s.common.ListenerSupport;
+import org.apache.shenyu.k8s.common.StatusMergePatch;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * Reconciler for Gateway resources (Gateway API v1).
+ *
+ * <p>Besides the Accepted condition, the reconciler reports Programmed and 
per-listener
+ * status (supportedKinds, attachedRoutes, per-listener Accepted/Programmed). 
A listener is
+ * usable only when it speaks plain HTTP on the port this gateway actually 
serves
+ * ({@code server.port}); anything else is reported with the spec-defined 
reason instead of
+ * being silently ignored. attachedRoutes reflects the in-memory bindings and 
converges on
+ * gateway resyncs.
+ */
+public class GatewayReconciler implements Reconciler {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(GatewayReconciler.class);
+
+    private final Lister<DynamicKubernetesObject> gatewayLister;
+
+    private final Lister<DynamicKubernetesObject> gatewayClassLister;
+
+    private final Lister<DynamicKubernetesObject> httpRouteLister;
+
+    private final ShenyuCacheRepository shenyuCacheRepository;
+
+    private final RateLimitingQueue<Request> httpRouteWorkQueue;
+
+    private final ApiClient apiClient;
+
+    /** The port the embedded ShenYu data plane actually listens on ({@code 
server.port}). */
+    private final int servedPort;
+
+    public GatewayReconciler(final 
SharedIndexInformer<DynamicKubernetesObject> gatewayInformer,
+                             final 
SharedIndexInformer<DynamicKubernetesObject> gatewayClassInformer,
+                             final 
SharedIndexInformer<DynamicKubernetesObject> httpRouteInformer,
+                             final ShenyuCacheRepository shenyuCacheRepository,
+                             final RateLimitingQueue<Request> 
httpRouteWorkQueue,
+                             final ApiClient apiClient,
+                             final int servedPort) {
+        this.gatewayLister = new Lister<>(gatewayInformer.getIndexer());
+        this.gatewayClassLister = new 
Lister<>(gatewayClassInformer.getIndexer());
+        this.httpRouteLister = new Lister<>(httpRouteInformer.getIndexer());
+        this.shenyuCacheRepository = shenyuCacheRepository;
+        this.httpRouteWorkQueue = httpRouteWorkQueue;
+        this.apiClient = apiClient;
+        this.servedPort = servedPort;
+    }
+
+    @Override
+    public Result reconcile(final Request request) {
+        LOG.debug("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 (!GatewayClassReconciler.isShenyuGateway(gateway, 
gatewayClassLister)) {
+                // Gateway conditions carry no controllerName, so an Accepted 
entry of another
+                // controller is indistinguishable from ours. Only Gateways 
ShenYu previously
+                // accepted (they have route bindings) are cleaned up and 
downgraded; touching
+                // anything else would fight the controller that owns it.
+                if (CollectionUtils.isEmpty(GatewayRouteCache.getInstance()
+                        .getRoutesByGateway(request.getNamespace(), 
request.getName()))) {
+                    LOG.debug("Gateway {} is not managed by ShenYu, skipping", 
request);
+                    return new Result(false);

Review Comment:
   A ShenYu-managed Gateway can legitimately have zero attached routes. If its 
GatewayClass is later repointed to another controller, this early return 
interprets the empty binding set as “never ours” and leaves ShenYu's existing 
`Accepted=True`/`Programmed=True` status indefinitely. Track Gateway ownership 
independently of route bindings, or recognize ShenYu's own status payload 
before deciding not to clear it.
   
   This issue also appears in the following locations of the same file:
   - line 127
   - line 293
   - line 299
   - line 357
   - line 397



##########
shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/HttpRouteParser.java:
##########
@@ -0,0 +1,626 @@
+/*
+ * 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.CoreV1EndpointPort;
+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.common.GatewayApiConstants;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.JsonFields;
+import org.apache.shenyu.k8s.common.ReferenceGrants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.UUID;
+import java.util.regex.Pattern;
+
+/**
+ * Parses an HTTPRoute into ShenYu divide selectors/rules. The parser is pure: 
it neither
+ * touches GatewayRouteCache nor the data plane, so a reconcile can compute 
status from a
+ * parse result without side effects.
+ *
+ * <p>Known divergences from the Gateway API spec, by design of the ShenYu 
matching model:
+ * match precedence is encoded in the selector sort only for the path 
dimension (exact beats
+ * longer prefix beats shorter prefix beats regex beats no-path); ShenYu 
evaluates the
+ * number of AND conditions before sort, so a match carrying more conditions 
still wins over
+ * a more specific path with fewer conditions.
+ */
+public class HttpRouteParser {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(HttpRouteParser.class);
+
+    /** Prefix isolating Gateway API IDs from the numeric ID space of the 
Ingress reconciler. */
+    private static final String ID_PREFIX = "gwapi-";
+
+    /** Stable hostname slot for rules without a hostname, keeping 
deterministic IDs well-defined. */
+    private static final String NO_HOSTNAME_PLACEHOLDER = "_";
+
+    /** Sort of an exact path match: highest precedence. Lower sort wins in 
ShenYu. */
+    private static final int SORT_EXACT_PATH = 100;
+
+    /** Base sort of a path prefix match; longer prefixes sort lower via 
length subtraction. */
+    private static final int SORT_PREFIX_BASE = 1000;
+
+    /** Cap of the prefix length subtracted from the base sort, keeping prefix 
sorts above exact. */
+    private static final int SORT_PREFIX_LENGTH_CAP = 800;
+
+    /** Sort of a regex path match: below any exact or prefix match. */
+    private static final int SORT_REGEX_PATH = 2000;
+
+    /** Sort of a rule without any path match: lowest precedence. */
+    private static final int SORT_NO_PATH = 3000;
+
+    private final Lister<V1Endpoints> endpointsLister;
+
+    private final Lister<DynamicKubernetesObject> referenceGrantLister;
+
+    public HttpRouteParser(final Lister<V1Endpoints> endpointsLister,
+                           final Lister<DynamicKubernetesObject> 
referenceGrantLister) {
+        this.endpointsLister = endpointsLister;
+        this.referenceGrantLister = referenceGrantLister;
+    }
+
+    /**
+     * Parse the HTTPRoute into a ShenYu config snapshot.
+     *
+     * @param httpRoute the route object
+     * @param hostnames effective hostnames (route hostnames intersected with 
the listener
+     *                  hostnames of every accepting Gateway); empty means 
"any host"
+     * @return the parsed config, never null
+     */
+    public ShenyuMemoryConfig parse(final DynamicKubernetesObject httpRoute, 
final List<String> hostnames) {
+        ShenyuMemoryConfig res = new ShenyuMemoryConfig();
+        String namespace = 
Objects.requireNonNull(httpRoute.getMetadata()).getNamespace();
+        String routeName = httpRoute.getMetadata().getName();
+        List<IngressConfiguration> routeConfigList = new ArrayList<>();
+        res.setRouteConfigList(routeConfigList);
+
+        JsonObject spec = JsonFields.getJsonObject(httpRoute.getRaw(), "spec");
+        if (Objects.nonNull(spec)) {
+            JsonArray rules = JsonFields.getJsonArray(spec, "rules");
+            ResolveState resolveState = new ResolveState();
+            for (int ruleIndex = 0; Objects.nonNull(rules) && ruleIndex < 
rules.size(); ruleIndex++) {
+                if (rules.get(ruleIndex).isJsonObject()) {
+                    processRule(rules.get(ruleIndex).getAsJsonObject(), 
hostnames, namespace, routeName, ruleIndex,
+                            routeConfigList, resolveState);
+                }
+            }
+            res.setAllBackendsResolved(!resolveState.anyUnresolved);
+            res.setUnresolvedReason(resolveState.reason);
+            res.setHasUnsupportedFilters(resolveState.unsupportedFilters);
+        }
+        return res;
+    }
+
+    private void processRule(final JsonObject rule, final List<String> 
hostnames, final String namespace,
+                             final String routeName, final int ruleIndex,
+                             final List<IngressConfiguration> routeConfigList,
+                             final ResolveState resolveState) {
+        // Filters are not implemented. Per the spec an unsupported filter 
MUST surface as
+        // Accepted=False/UnsupportedValue and the rule MUST NOT be applied 
partially.
+        JsonArray filters = JsonFields.getJsonArray(rule, "filters");
+        if (Objects.nonNull(filters) && !filters.isEmpty()) {
+            resolveState.unsupportedFilters = true;
+            LOG.warn("HTTPRoute {}/{} rule {} declares filters which are not 
supported; the rule is not programmed",
+                    namespace, routeName, ruleIndex);
+            return;
+        }
+
+        // A rule without backendRefs has no ShenYu equivalent; skipping it 
leaves matching
+        // requests unmatched instead of programming an empty upstream list.
+        JsonArray backendRefs = JsonFields.getJsonArray(rule, "backendRefs");
+        if (Objects.isNull(backendRefs) || backendRefs.isEmpty()) {
+            return;
+        }
+
+        BackendResolveResult result = parseBackendRefs(backendRefs, namespace, 
routeName);
+        List<DivideUpstream> upstreamList = result.upstreams;
+        if (result.unresolvedCount > 0) {
+            resolveState.anyUnresolved = true;
+            if (Objects.isNull(resolveState.reason)) {
+                resolveState.reason = result.unresolvedReason;
+            }
+            LOG.warn("HTTPRoute {}/{} rule {} has {} unresolved backendRef(s)",
+                    namespace, routeName, ruleIndex, result.unresolvedCount);
+        }
+
+        // An empty (handle="[]") selector would make matching requests 5xx; 
no match is safer
+        if (upstreamList.isEmpty()) {
+            return;
+        }
+
+        // One selector+rule per hostname: a request matches at most one 
hostname, and the
+        // selector's AND semantics cannot express "any of these hostnames".
+        JsonArray matches = JsonFields.getJsonArray(rule, "matches");
+        if (Objects.nonNull(matches) && !matches.isEmpty()) {
+            for (int matchIndex = 0; matchIndex < matches.size(); 
matchIndex++) {
+                if (!matches.get(matchIndex).isJsonObject()) {
+                    continue;
+                }
+                JsonObject match = matches.get(matchIndex).getAsJsonObject();
+                List<ConditionData> matchConditions = new ArrayList<>();
+                appendMatchConditions(matchConditions, match);
+                int sort = pathSort(match);
+                if (hostnames.isEmpty()) {
+                    addSelectorRule(routeConfigList, namespace, routeName, 
ruleIndex, null,
+                            matchIndex, sort, matchConditions, upstreamList);
+                } else {
+                    for (String hostname : hostnames) {
+                        addSelectorRule(routeConfigList, namespace, routeName, 
ruleIndex,
+                                hostname, matchIndex, sort,
+                                composeConditions(hostname, matchConditions), 
upstreamList);
+                    }
+                }
+            }
+        } else {
+            if (hostnames.isEmpty()) {
+                addSelectorRule(routeConfigList, namespace, routeName, 
ruleIndex, null,
+                        0, SORT_NO_PATH, new ArrayList<>(), upstreamList);
+            } else {
+                for (String hostname : hostnames) {
+                    addSelectorRule(routeConfigList, namespace, routeName, 
ruleIndex,
+                            hostname, 0, SORT_NO_PATH,
+                            composeConditions(hostname, new ArrayList<>()), 
upstreamList);
+                }
+            }
+        }
+    }
+
+    private void addSelectorRule(final List<IngressConfiguration> 
routeConfigList,
+                                 final String namespace, final String 
routeName, final int ruleIndex,
+                                 final String hostname, final int matchIndex, 
final int sort,
+                                 final List<ConditionData> conditions, final 
List<DivideUpstream> upstreamList) {
+        // A CUSTOM_FLOW selector with an empty condition list never matches 
in ShenYu, so a
+        // rule without matches (spec: matches everything, like PathPrefix /) 
needs an
+        // explicit match-all condition.
+        if (conditions.isEmpty()) {
+            conditions.add(matchAllCondition());
+        }
+        String selectorId = deterministicSelectorId(namespace, routeName, 
ruleIndex, hostname, matchIndex);
+        String ruleId = deterministicRuleId(selectorId, matchIndex);
+        String hostComponent = Objects.isNull(hostname) ? "" : "-" + hostname;
+        String selectorName = routeName + "-rule-" + ruleIndex + hostComponent 
+ "-m" + matchIndex;
+        SelectorData selectorData = buildSelectorData(selectorId, 
selectorName, sort, conditions, upstreamList);
+        RuleData ruleData = buildRuleData(ruleId, selectorId, selectorName, 
conditions);
+        routeConfigList.add(new IngressConfiguration(selectorData, 
List.of(ruleData), null));
+    }
+
+    /** Every request path starts with '/', so this condition matches all 
requests. */
+    private ConditionData matchAllCondition() {
+        ConditionData condition = new ConditionData();
+        condition.setParamType(ParamTypeEnum.URI.getName());
+        condition.setOperator(OperatorEnum.STARTS_WITH.getAlias());
+        condition.setParamValue("/");
+        return condition;
+    }
+
+    private List<ConditionData> composeConditions(final String hostname,
+                                                  final List<ConditionData> 
matchConditions) {
+        List<ConditionData> conditions = new ArrayList<>();
+        conditions.add(buildHostnameCondition(hostname));
+        conditions.addAll(matchConditions);
+        return conditions;
+    }
+
+    /**
+     * Deterministic selector ID derived from the route coordinates, so the 
same spec always
+     * yields the same ID and a resync upserts instead of delete-then-create 
on the data plane.
+     */
+    private String deterministicSelectorId(final String namespace, final 
String routeName, final int ruleIndex,
+                                           final String hostname, final int 
matchIndex) {
+        String hostComponent = Objects.isNull(hostname) ? 
NO_HOSTNAME_PLACEHOLDER : hostname;
+        String key = namespace + "/" + routeName + "/r" + ruleIndex + "/h" + 
hostComponent + "/m" + matchIndex;
+        return ID_PREFIX + 
UUID.nameUUIDFromBytes(key.getBytes(StandardCharsets.UTF_8));
+    }
+
+    /** Derive a deterministic rule ID from its parent selector ID; stays 
under varchar(128). */
+    private String deterministicRuleId(final String selectorId, final int 
matchIndex) {
+        return selectorId + "/rule-m" + matchIndex;
+    }
+
+    /**
+     * Exact hostnames use EQ. A wildcard ({@code *.example.com}) is a suffix 
match per the
+     * Gateway API spec: it matches {@code test.example.com} and {@code 
foo.test.example.com}
+     * but not {@code example.com} — impossible to express with EQ's exact 
comparison, hence REGEX.
+     */
+    private ConditionData buildHostnameCondition(final String hostname) {
+        ConditionData condition = new ConditionData();
+        condition.setParamType(ParamTypeEnum.DOMAIN.getName());
+        if (hostname.startsWith("*.")) {
+            String suffix = hostname.substring(2).replace(".", "\\.");
+            condition.setOperator(OperatorEnum.REGEX.getAlias());
+            condition.setParamValue("^([^.]+\\.)+" + suffix + "$");
+        } else {
+            condition.setOperator(OperatorEnum.EQ.getAlias());
+            condition.setParamValue(hostname);
+        }
+        return condition;
+    }
+
+    private SelectorData buildSelectorData(final String selectorId, final 
String selectorName, final int sort,
+                                           final List<ConditionData> 
conditions, final List<DivideUpstream> upstreamList) {
+        return SelectorData.builder()
+                .id(selectorId)
+                .pluginId(String.valueOf(PluginEnum.DIVIDE.getCode()))
+                .pluginName(PluginEnum.DIVIDE.getName())
+                .name(selectorName)
+                .sort(sort)
+                .matchMode(MatchModeEnum.AND.getCode())
+                .type(SelectorTypeEnum.CUSTOM_FLOW.getCode())
+                .enabled(true)
+                .logged(false)
+                .continued(true)
+                .conditionList(conditions)
+                .handle(GsonUtils.getInstance().toJson(upstreamList))
+                .build();
+    }
+
+    private RuleData buildRuleData(final String ruleId, final String 
selectorId,
+                                   final String selectorName, final 
List<ConditionData> conditions) {
+        DivideRuleHandle divideRuleHandle = new DivideRuleHandle();
+        divideRuleHandle.setLoadBalance(LoadBalanceEnum.RANDOM.getName());
+        divideRuleHandle.setRetry(3);
+        divideRuleHandle.setTimeout(3000L);
+
+        return RuleData.builder()
+                .id(ruleId)
+                .selectorId(selectorId)
+                .name(selectorName)
+                .pluginName(PluginEnum.DIVIDE.getName())
+                .sort(1)
+                .matchMode(MatchModeEnum.AND.getCode())
+                .conditionDataList(conditions)
+                .handle(GsonUtils.getInstance().toJson(divideRuleHandle))
+                .loged(false)
+                .enabled(true)
+                .build();
+    }
+
+    /**
+     * Resolve the rule's backendRefs into upstream addresses. Service is the 
only supported
+     * kind (the default when absent); anything else is unresolved with reason 
InvalidKind.
+     * A cross-namespace Service requires a ReferenceGrant in that namespace; 
a Service whose
+     * Endpoints are missing or have no ready addresses is unresolved with 
reason
+     * BackendNotFound. Both drive ResolvedRefs=False.
+     */
+    private BackendResolveResult parseBackendRefs(final JsonArray backendRefs, 
final String namespace,
+                                                  final String routeName) {
+        List<DivideUpstream> upstreamList = new ArrayList<>();
+        int unresolvedCount = 0;
+        String unresolvedReason = null;
+        for (JsonElement element : backendRefs) {
+            if (!element.isJsonObject()) {
+                continue;
+            }
+            BackendRefOutcome outcome = 
resolveBackendRef(element.getAsJsonObject(), namespace, routeName);
+            if (Objects.nonNull(outcome.unresolvedReason)) {
+                unresolvedCount++;
+                if (Objects.isNull(unresolvedReason)) {
+                    unresolvedReason = outcome.unresolvedReason;

Review Comment:
   Unresolved backendRefs are dropped while resolved ones receive all traffic. 
Gateway API requires the proportion assigned to an invalid backendRef to 
receive an HTTP 500; silently renormalizing a 50/50 route to 100% healthy 
traffic changes routing semantics. Preserve invalid backends' weighted share 
with an explicit failure path rather than omitting them.
   
   This issue also appears in the following locations of the same file:
   - line 415
   - line 447
   - line 526



-- 
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