This is an automated email from the ASF dual-hosted git repository.

Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git


The following commit(s) were added to refs/heads/master by this push:
     new 01766dcf54 Fixes #6484: Guard missing Dubbo ingress endpoints (#7220)
01766dcf54 is described below

commit 01766dcf544d63169f41cbbf042b4fb24ed4dca3
Author: BobSong <[email protected]>
AuthorDate: Sat Oct 10 09:45:58 2026 +0800

    Fixes #6484: Guard missing Dubbo ingress endpoints (#7220)
    
    * fix(kubernetes-controller): guard missing Dubbo ingress endpoints
    
    * fix(k8s): retry Dubbo ingress until endpoints are ready
    
    ---------
    
    Co-authored-by: BobSong-dev <[email protected]>
---
 .../shenyu/k8s/parser/DubboIngressParser.java      | 16 ++++++-
 .../k8s/parser/EndpointsUnavailableException.java  | 36 ++++++++++++++++
 .../shenyu/k8s/reconciler/IngressReconciler.java   | 32 +++++++-------
 .../org/apache/shenyu/k8s/DubboReconcilerTest.java | 38 ++++++++++++++++-
 .../shenyu/k8s/parser/DubboIngressParserTest.java  | 49 ++++++++++++++++++++++
 5 files changed, 151 insertions(+), 20 deletions(-)

diff --git 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DubboIngressParser.java
 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DubboIngressParser.java
index 7554451d88..b4afe5419b 100644
--- 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DubboIngressParser.java
+++ 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DubboIngressParser.java
@@ -159,6 +159,9 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
             String serviceName = defaultBackend.getService().getName();
             // shenyu routes directly to the container
             V1Endpoints v1Endpoints = 
endpointsLister.namespace(namespace).get(serviceName);
+            if (Objects.isNull(v1Endpoints)) {
+                throw new EndpointsUnavailableException(namespace, 
serviceName);
+            }
             List<V1EndpointSubset> subsets = v1Endpoints.getSubsets();
             if (Objects.isNull(subsets) || CollectionUtils.isEmpty(subsets)) {
                 LOG.info("Endpoints {} do not have subsets", serviceName);
@@ -171,7 +174,7 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
                     for (V1EndpointAddress address : addresses) {
                         String upstreamIp = address.getIp();
                         String defaultPort = 
parsePort(defaultBackend.getService());
-                        if (Objects.nonNull(defaultPort)) {
+                        if (StringUtils.isNotBlank(upstreamIp) && 
Objects.nonNull(defaultPort)) {
                             DubboUpstream upstream = DubboUpstream.builder()
                                     .upstreamUrl(upstreamIp + ":" + 
defaultPort)
                                     .weight(100)
@@ -185,6 +188,9 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
                     }
                 }
             }
+            if (dubboUpstreamList.isEmpty()) {
+                throw new EndpointsUnavailableException(namespace, 
serviceName);
+            }
         }
         return dubboUpstreamList;
     }
@@ -346,6 +352,9 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
                 && Objects.nonNull(path.getBackend().getService()) && 
Objects.nonNull(path.getBackend().getService().getName())) {
             String serviceName = path.getBackend().getService().getName();
             V1Endpoints v1Endpoints = 
endpointsLister.namespace(namespace).get(serviceName);
+            if (Objects.isNull(v1Endpoints)) {
+                throw new EndpointsUnavailableException(namespace, 
serviceName);
+            }
             List<V1EndpointSubset> subsets = v1Endpoints.getSubsets();
             String[] protocols = null;
             if (Objects.nonNull(annotations) && 
annotations.containsKey(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY)
@@ -364,7 +373,7 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
                         V1EndpointAddress address = addresses.get(i);
                         String upstreamIp = address.getIp();
                         String defaultPort = 
parsePort(path.getBackend().getService());
-                        if (Objects.nonNull(defaultPort)) {
+                        if (StringUtils.isNotBlank(upstreamIp) && 
Objects.nonNull(defaultPort)) {
                             String upstreamProtocol = 
Objects.isNull(protocols) || i >= protocols.length ? "dubbo://" : protocols[i];
                             DubboUpstream upstream = DubboUpstream.builder()
                                     .upstreamUrl(upstreamIp + ":" + 
defaultPort)
@@ -379,6 +388,9 @@ public class DubboIngressParser implements 
K8sResourceParser<V1Ingress> {
                     }
                 }
             }
+            if (upstreamList.isEmpty()) {
+                throw new EndpointsUnavailableException(namespace, 
serviceName);
+            }
         }
         return upstreamList;
     }
diff --git 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/EndpointsUnavailableException.java
 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/EndpointsUnavailableException.java
new file mode 100644
index 0000000000..455a1cb7ec
--- /dev/null
+++ 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/EndpointsUnavailableException.java
@@ -0,0 +1,36 @@
+/*
+ * 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;
+
+/**
+ * Indicates that an ingress must be retried when its backend becomes 
available.
+ */
+public class EndpointsUnavailableException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Create a retryable backend availability failure.
+     *
+     * @param namespace backend namespace
+     * @param serviceName backend service
+     */
+    public EndpointsUnavailableException(final String namespace, final String 
serviceName) {
+        super("No ready Dubbo endpoints for " + namespace + "/" + serviceName);
+    }
+}
diff --git 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
index fb51ab847c..07b44eb4c8 100644
--- 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
+++ 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/reconciler/IngressReconciler.java
@@ -62,6 +62,7 @@ import org.apache.shenyu.k8s.common.ServiceIngressRelation;
 import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
 import org.apache.shenyu.k8s.parser.IngressPluginDefinition;
 import org.apache.shenyu.k8s.parser.IngressParser;
+import org.apache.shenyu.k8s.parser.EndpointsUnavailableException;
 import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -175,22 +176,21 @@ public class IngressReconciler implements Reconciler {
             return new Result(false);
         }
 
-        if (Objects.isNull(oldIngress)) {
+        if (Objects.isNull(oldIngress) || needUpdate(oldIngress, v1Ingress)) {
             try {
-                addNewIngressConfigToShenyu(v1Ingress, new 
CoreV1Api(apiClient));
-            } catch (IOException e) {
-                LOG.error("add new ingress config error", e);
-            }
-        } else if (needUpdate(oldIngress, v1Ingress)) {
-            // Update logic
-            // 1. clean old config
-            doDeleteConfigByIngress(request, oldIngress);
-
-            // 2. add new config
-            try {
-                addNewIngressConfigToShenyu(v1Ingress, new 
CoreV1Api(apiClient));
+                // Resolve all backends before replacing the last successfully 
applied configuration.
+                V1Ingress ingressCopy = new 
V1IngressBuilder(v1Ingress).build();
+                List<ShenyuMemoryConfig> configs = 
ingressParser.parse(ingressCopy, new CoreV1Api(apiClient));
+                if (Objects.nonNull(oldIngress)) {
+                    doDeleteConfigByIngress(request, oldIngress);
+                }
+                addNewIngressConfigToShenyu(v1Ingress, configs);
+            } catch (EndpointsUnavailableException e) {
+                LOG.info("Retry ingress {}: {}", request, e.getMessage());
+                return new Result(true);
             } catch (IOException e) {
                 LOG.error("add new ingress config error", e);
+                return new Result(true);
             }
         }
         IngressCache.getInstance().put(request.getNamespace(), 
request.getName(), v1Ingress);
@@ -469,10 +469,8 @@ public class IngressReconciler implements Reconciler {
         return !oldIngress.equals(currentIngress);
     }
 
-    private void addNewIngressConfigToShenyu(final V1Ingress v1Ingress, final 
CoreV1Api apiClient) throws IOException {
-        V1Ingress ingressCopy = new V1IngressBuilder(v1Ingress).build();
-        List<ShenyuMemoryConfig> shenyuMemoryConfigList = 
ingressParser.parse(ingressCopy, apiClient);
-        String pluginName = getPluginName(ingressCopy);
+    private void addNewIngressConfigToShenyu(final V1Ingress v1Ingress, final 
List<ShenyuMemoryConfig> shenyuMemoryConfigList) throws IOException {
+        String pluginName = getPluginName(v1Ingress);
 
         for (ShenyuMemoryConfig shenyuMemoryConfig : shenyuMemoryConfigList) {
             if (Objects.nonNull(shenyuMemoryConfig)) {
diff --git 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/DubboReconcilerTest.java
 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/DubboReconcilerTest.java
index 0507d71b6e..50220f60ab 100644
--- 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/DubboReconcilerTest.java
+++ 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/DubboReconcilerTest.java
@@ -82,7 +82,7 @@ public final class DubboReconcilerTest {
 
     @BeforeEach
     public void init() {
-        IngressCache.getInstance().remove("mockedNamespace", "mockedIngress");
+        IngressCache.getInstance().remove(NAMESPACE, INGRESS_NAME);
         ingressInformer = mock(SharedIndexInformer.class);
         secretInformer = mock(SharedIndexInformer.class);
         shenyuCacheRepository = mock(ShenyuCacheRepository.class);
@@ -163,6 +163,42 @@ public final class DubboReconcilerTest {
         verify(shenyuCacheRepository).saveOrUpdateMetaData(any());
     }
 
+    @Test
+    public void testIngressBeforeEndpointsRecoversOnRetry() {
+        Indexer<V1Endpoints> indexer = endpointsInformer.getIndexer();
+        final V1Endpoints ready = indexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME);
+        when(indexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(null);
+        Request request = new Request(NAMESPACE, INGRESS_NAME);
+
+        Assertions.assertEquals(new Result(true), 
ingressReconciler.reconcile(request));
+        org.mockito.Mockito.verify(shenyuCacheRepository, 
org.mockito.Mockito.never()).saveOrUpdateSelectorData(any());
+        Assertions.assertNull(IngressCache.getInstance().get(NAMESPACE, 
INGRESS_NAME));
+
+        when(indexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(ready);
+        Assertions.assertEquals(new Result(false), 
ingressReconciler.reconcile(request));
+        
verify(shenyuCacheRepository).saveOrUpdateSelectorData(org.mockito.ArgumentMatchers.argThat(selector
 ->
+                selector.getHandle().contains("127.0.0.1:20888")));
+        verify(shenyuCacheRepository).saveOrUpdateRuleData(any());
+        verify(shenyuCacheRepository).saveOrUpdateMetaData(any());
+    }
+
+    @Test
+    public void testUnavailableUpdatePreservesAppliedIngress() {
+        Request request = new Request(NAMESPACE, INGRESS_NAME);
+        Assertions.assertEquals(new Result(false), 
ingressReconciler.reconcile(request));
+        final V1Ingress applied = IngressCache.getInstance().get(NAMESPACE, 
INGRESS_NAME);
+        V1Ingress updated = new V1IngressBuilder(applied).build();
+        updated.getMetadata().setResourceVersion("2");
+        when(ingressInformer.getIndexer().getByKey(NAMESPACE + "/" + 
INGRESS_NAME)).thenReturn(updated);
+        when(endpointsInformer.getIndexer().getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(null);
+        org.mockito.Mockito.clearInvocations(shenyuCacheRepository);
+
+        Assertions.assertEquals(new Result(true), 
ingressReconciler.reconcile(request));
+        Assertions.assertSame(applied, 
IngressCache.getInstance().get(NAMESPACE, INGRESS_NAME));
+        org.mockito.Mockito.verify(shenyuCacheRepository, 
org.mockito.Mockito.never()).deleteSelectorData(any(), any());
+        org.mockito.Mockito.verify(shenyuCacheRepository, 
org.mockito.Mockito.never()).saveOrUpdateSelectorData(any());
+    }
+
     @Test
     public void testParseSkipsContextPathWhenDubboEnabled() {
         Map<String, String> annotations = new HashMap<>();
diff --git 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DubboIngressParserTest.java
 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DubboIngressParserTest.java
index 6907b28777..43b8c6baf4 100644
--- 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DubboIngressParserTest.java
+++ 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DubboIngressParserTest.java
@@ -25,9 +25,12 @@ import io.kubernetes.client.openapi.models.V1Endpoints;
 import io.kubernetes.client.openapi.models.V1EndpointsBuilder;
 import io.kubernetes.client.openapi.models.V1HTTPIngressPathBuilder;
 import io.kubernetes.client.openapi.models.V1Ingress;
+import io.kubernetes.client.openapi.models.V1IngressBackend;
 import io.kubernetes.client.openapi.models.V1IngressBuilder;
 import io.kubernetes.client.openapi.models.V1IngressRuleBuilder;
+import io.kubernetes.client.openapi.models.V1IngressServiceBackend;
 import io.kubernetes.client.openapi.models.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceBackendPort;
 import org.apache.shenyu.common.dto.convert.selector.DubboUpstream;
 import org.apache.shenyu.common.utils.GsonUtils;
 import org.apache.shenyu.k8s.common.IngressConstants;
@@ -160,6 +163,52 @@ public class DubboIngressParserTest {
         assertNotNull(upstreams);
     }
 
+    @Test
+    public void shouldRetryPathWhenEndpointsAreMissing() {
+        Indexer<V1Endpoints> missingEndpointsIndexer = mock(Indexer.class);
+        DubboIngressParser parser = new DubboIngressParser(serviceLister, new 
Lister<>(missingEndpointsIndexer));
+
+        Assertions.assertThrows(EndpointsUnavailableException.class, () -> 
parser.parse(
+                createIngress(null, Collections.emptyMap(), true), null));
+    }
+
+    @Test
+    public void shouldRetryDefaultBackendWhenEndpointsAreMissing() {
+        Indexer<V1Endpoints> missingEndpointsIndexer = mock(Indexer.class);
+        DubboIngressParser parser = new DubboIngressParser(serviceLister, new 
Lister<>(missingEndpointsIndexer));
+        V1Ingress ingress = new 
V1IngressBuilder().withNewMetadata().withName("test-ingress").withNamespace(NAMESPACE)
+                
.withAnnotations(Collections.emptyMap()).withLabels(Collections.emptyMap()).endMetadata()
+                .withNewSpec().withDefaultBackend(new V1IngressBackend()
+                        .service(new 
V1IngressServiceBackend().name(SERVICE_NAME)
+                                .port(new 
V1ServiceBackendPort().number(8080))))
+                .endSpec().build();
+
+        Assertions.assertThrows(EndpointsUnavailableException.class, () -> 
parser.parse(ingress, null));
+    }
+
+    @Test
+    public void shouldNotInheritDefaultWhenPathEndpointsAreUnavailable() {
+        V1Ingress ingress = createIngress(null, Collections.emptyMap(), true);
+        ingress.getSpec().setDefaultBackend(new V1IngressBackend().service(new 
V1IngressServiceBackend()
+                .name("ready-default").port(new 
V1ServiceBackendPort().number(8080))));
+        when(endpointsIndexer.getByKey(NAMESPACE + 
"/ready-default")).thenReturn(new V1EndpointsBuilder()
+                .withSubsets(new V1EndpointSubsetBuilder().withAddresses(new 
V1EndpointAddress().ip("127.0.0.2")).build()).build());
+        when(endpointsIndexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(null);
+        DubboIngressParser parser = new DubboIngressParser(serviceLister, 
endpointsLister);
+        Assertions.assertThrows(EndpointsUnavailableException.class, () -> 
parser.parse(ingress, null));
+    }
+
+    @Test
+    public void shouldRetryUntilEndpointsHaveReadyAddresses() {
+        V1Ingress ingress = createIngress(null, Collections.emptyMap(), true);
+        DubboIngressParser parser = new DubboIngressParser(serviceLister, 
endpointsLister);
+        when(endpointsIndexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(new V1Endpoints());
+        Assertions.assertThrows(EndpointsUnavailableException.class, () -> 
parser.parse(ingress, null));
+        when(endpointsIndexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(new V1EndpointsBuilder()
+                .withSubsets(new 
V1EndpointSubsetBuilder().withNotReadyAddresses(new 
V1EndpointAddress().ip("127.0.0.1")).build()).build());
+        Assertions.assertThrows(EndpointsUnavailableException.class, () -> 
parser.parse(ingress, null));
+    }
+
     @Test
     public void shouldParseIngressWhenAnnotationsAreNull() {
         ShenyuMemoryConfig config = assertDoesNotThrow(() -> 
createParser().parse(

Reply via email to