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 96f7215dd8 fix: tolerate absent annotations when reconciling ingress 
(#6491) (#7285)
96f7215dd8 is described below

commit 96f7215dd8074819289c1044c4f2c90631557686
Author: wy471x <[email protected]>
AuthorDate: Sat Sep 26 11:24:16 2026 +0800

    fix: tolerate absent annotations when reconciling ingress (#6491) (#7285)
---
 .../apache/shenyu/k8s/parser/IngressParser.java    |   5 +-
 .../shenyu/k8s/reconciler/IngressReconciler.java   |  49 ++++---
 .../IngressReconcilerMissingAnnotationsTest.java   | 146 +++++++++++++++++++++
 3 files changed, 183 insertions(+), 17 deletions(-)

diff --git 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
index 560e333293..c4cdbcc663 100644
--- 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
+++ 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressParser.java
@@ -25,6 +25,7 @@ import io.kubernetes.client.openapi.models.V1HTTPIngressPath;
 import io.kubernetes.client.openapi.models.V1Ingress;
 import io.kubernetes.client.openapi.models.V1IngressRule;
 import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.commons.collections4.MapUtils;
 import org.apache.shenyu.k8s.common.IngressConstants;
 import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
 import org.slf4j.Logger;
@@ -32,6 +33,7 @@ import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 
 /**
@@ -95,7 +97,8 @@ public class IngressParser implements 
K8sResourceListParser<V1Ingress> {
     }
 
     private boolean getBooleanAnnotation(final V1Ingress ingress, final String 
annotationKey) {
-        String annotationValue = 
ingress.getMetadata().getAnnotations().get(annotationKey);
+        Map<String, String> annotations = 
Objects.isNull(ingress.getMetadata()) ? null : 
ingress.getMetadata().getAnnotations();
+        String annotationValue = 
MapUtils.emptyIfNull(annotations).get(annotationKey);
         return Objects.nonNull(annotationValue) && 
Boolean.parseBoolean(annotationValue);
     }
 
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 20874940e1..9d44018074 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
@@ -34,6 +34,7 @@ import io.kubernetes.client.openapi.models.V1IngressBuilder;
 import io.kubernetes.client.openapi.models.V1IngressRule;
 import io.kubernetes.client.openapi.models.V1Secret;
 import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.MapUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
 import org.apache.shenyu.common.config.ssl.SslCrtAndKeyStream;
@@ -62,6 +63,7 @@ import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -158,7 +160,7 @@ public class IngressReconciler implements Reconciler {
             }
             return new Result(false);
         }
-        Map<String, String> annotations = 
v1Ingress.getMetadata().getAnnotations();
+        Map<String, String> annotations = getAnnotations(v1Ingress);
         enablePluginsBasedOnAnnotations(annotations, request);
 
         if (!checkIngressClass(v1Ingress)) {
@@ -214,28 +216,29 @@ public class IngressReconciler implements Reconciler {
     }
 
     private void doDeleteConfigByIngress(final Request request, final 
V1Ingress oldIngress) {
+        final Map<String, String> annotations = getAnnotations(oldIngress);
         List<String> selectorList = new ArrayList<>();
-        if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_DUBBO_ENABLED),
 "true")) {
+        if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.DUBBO.getName(),
-                    
oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_DUBBO_CONTEXT_PATH));
-        } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED),
 "true")) {
+                    
annotations.get(IngressConstants.PLUGIN_DUBBO_CONTEXT_PATH));
+        } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED), 
"true")) {
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.WEB_SOCKET.getName(), "");
-        } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_GRPC_ENABLED),
 "true")) {
+        } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_GRPC_ENABLED), "true")) 
{
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.GRPC.getName(), "");
-        } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_SOFA_ENABLED),
 "true")) {
+        } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_SOFA_ENABLED), "true")) 
{
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.SOFA.getName(),
-                    
oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_SOFA_CONTEXT_PATH));
+                    
annotations.get(IngressConstants.PLUGIN_SOFA_CONTEXT_PATH));
         } else {
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.DIVIDE.getName(), "");
         }
         if (Objects.nonNull(selectorList) && !selectorList.isEmpty()) {
-            if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_DUBBO_ENABLED),
 "true")) {
+            if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.DUBBO.getName());
-            } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED),
 "true")) {
+            } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED), 
"true")) {
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.WEB_SOCKET.getName());
-            } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_GRPC_ENABLED),
 "true")) {
+            } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_GRPC_ENABLED), "true")) 
{
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.GRPC.getName());
-            } else if 
(Objects.equals(oldIngress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_SOFA_ENABLED),
 "true")) {
+            } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_SOFA_ENABLED), "true")) 
{
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.SOFA.getName());
             } else {
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.DIVIDE.getName());
@@ -687,13 +690,27 @@ public class IngressReconciler implements Reconciler {
         return res;
     }
 
+    /**
+     * Get the annotations of the ingress, an absent metadata or annotations 
is treated as an empty annotation map.
+     *
+     * @param ingress ingress resource
+     * @return annotations of the ingress, never null
+     */
+    private Map<String, String> getAnnotations(final V1Ingress ingress) {
+        if (Objects.isNull(ingress) || Objects.isNull(ingress.getMetadata())) {
+            return Collections.emptyMap();
+        }
+        return MapUtils.emptyIfNull(ingress.getMetadata().getAnnotations());
+    }
+
     private String getPluginName(final V1Ingress ingress) {
+        Map<String, String> annotations = getAnnotations(ingress);
         String pluginName;
-        String pluginDubboEnabled = 
ingress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_DUBBO_ENABLED);
-        String pluginWebSocketEnabled = 
ingress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED);
-        String pluginBrpcEnabled = 
ingress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_BRPC_ENABLED);
-        String pluginGrpcEnabled = 
ingress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_GRPC_ENABLED);
-        String pluginSofaEnabled = 
ingress.getMetadata().getAnnotations().get(IngressConstants.PLUGIN_SOFA_ENABLED);
+        String pluginDubboEnabled = 
annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED);
+        String pluginWebSocketEnabled = 
annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED);
+        String pluginBrpcEnabled = 
annotations.get(IngressConstants.PLUGIN_BRPC_ENABLED);
+        String pluginGrpcEnabled = 
annotations.get(IngressConstants.PLUGIN_GRPC_ENABLED);
+        String pluginSofaEnabled = 
annotations.get(IngressConstants.PLUGIN_SOFA_ENABLED);
         if ((Boolean.TRUE.toString()).equals(pluginDubboEnabled)) {
             pluginName = PluginEnum.DUBBO.getName();
         } else if ((Boolean.TRUE.toString()).equals(pluginWebSocketEnabled)) {
diff --git 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerMissingAnnotationsTest.java
 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerMissingAnnotationsTest.java
new file mode 100644
index 0000000000..f28be557b3
--- /dev/null
+++ 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerMissingAnnotationsTest.java
@@ -0,0 +1,146 @@
+/*
+ * 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;
+
+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.Indexer;
+import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.models.CoreV1EndpointPort;
+import io.kubernetes.client.openapi.models.V1EndpointAddress;
+import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
+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.V1IngressBuilder;
+import io.kubernetes.client.openapi.models.V1IngressRule;
+import io.kubernetes.client.openapi.models.V1IngressRuleBuilder;
+import io.kubernetes.client.openapi.models.V1Secret;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
+import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.k8s.cache.IngressCache;
+import org.apache.shenyu.k8s.cache.IngressSelectorCache;
+import org.apache.shenyu.k8s.cache.ServiceIngressCache;
+import org.apache.shenyu.k8s.parser.IngressParser;
+import org.apache.shenyu.k8s.reconciler.IngressReconciler;
+import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for reconciling an ingress that does not carry any annotation, such an 
ingress selects
+ * the controller with spec.ingressClassName instead of the 
kubernetes.io/ingress.class annotation.
+ */
+public final class IngressReconcilerMissingAnnotationsTest {
+
+    private static final String NAMESPACE = "missing-annotations-ns";
+
+    private static final String INGRESS_NAME = "missing-annotations-ingress";
+
+    private static final String SERVICE_NAME = "missing-annotations-service";
+
+    private Indexer<V1Ingress> ingressIndexer;
+
+    private ShenyuCacheRepository shenyuCacheRepository;
+
+    private IngressReconciler ingressReconciler;
+
+    @BeforeEach
+    public void init() {
+        final SharedIndexInformer<V1Ingress> ingressInformer = 
mock(SharedIndexInformer.class);
+        final SharedIndexInformer<V1Secret> secretInformer = 
mock(SharedIndexInformer.class);
+        final SharedIndexInformer<V1Service> serviceInformer = 
mock(SharedIndexInformer.class);
+        final SharedIndexInformer<V1Endpoints> endpointsInformer = 
mock(SharedIndexInformer.class);
+        shenyuCacheRepository = mock(ShenyuCacheRepository.class);
+
+        ingressIndexer = mock(Indexer.class);
+        V1IngressRule mockedRule = new 
V1IngressRuleBuilder().withNewHttp().withPaths(
+                        new 
V1HTTPIngressPathBuilder().withPath("/**").withPathType("ImplementationSpecific")
+                                .withNewBackend()
+                                    
.withNewService().withName(SERVICE_NAME).withNewPort().withNumber(8189).endPort().endService()
+                                .endBackend().build())
+                .endHttp().build();
+        V1Ingress mockedIngress = new 
V1IngressBuilder().withNewMetadata().withName(INGRESS_NAME).withNamespace(NAMESPACE).endMetadata()
+                
.withNewSpec().withIngressClassName("shenyu").withRules(mockedRule).endSpec()
+                .withKind("Ingress").build();
+        when(ingressIndexer.getByKey(NAMESPACE + "/" + 
INGRESS_NAME)).thenReturn(mockedIngress);
+        when(ingressInformer.getIndexer()).thenReturn(ingressIndexer);
+
+        Indexer<V1Endpoints> endpointsIndexer = mock(Indexer.class);
+        V1Endpoints mockedEndpoints = new 
V1EndpointsBuilder().withKind("Endpoints")
+                
.withNewMetadata().withNamespace(NAMESPACE).withName(SERVICE_NAME).endMetadata()
+                .withSubsets(new V1EndpointSubsetBuilder()
+                        .withAddresses(new V1EndpointAddress().ip("127.0.0.1"))
+                        .withPorts(new 
CoreV1EndpointPort().port(8189).protocol("TCP"))
+                        .build())
+                .build();
+        when(endpointsIndexer.getByKey(NAMESPACE + "/" + 
SERVICE_NAME)).thenReturn(mockedEndpoints);
+        when(endpointsInformer.getIndexer()).thenReturn(endpointsIndexer);
+
+        IngressParser ingressParser = new IngressParser(serviceInformer, 
endpointsInformer);
+        ingressReconciler = new IngressReconciler(ingressInformer, 
secretInformer, shenyuCacheRepository,
+                new ShenyuSniAsyncMapping(), ingressParser, 
mock(ApiClient.class));
+    }
+
+    /**
+     * test reconcile an ingress without annotations.
+     */
+    @Test
+    public void testReconcileIngressWithoutAnnotations() {
+        Result result = Assertions.assertDoesNotThrow(
+                () -> ingressReconciler.reconcile(new Request(NAMESPACE, 
INGRESS_NAME)));
+
+        Assertions.assertEquals(new Result(false), result);
+        verify(shenyuCacheRepository, 
atLeastOnce()).saveOrUpdateSelectorData(any());
+        Assertions.assertNotNull(IngressCache.getInstance().get(NAMESPACE, 
INGRESS_NAME));
+        
Assertions.assertFalse(ServiceIngressCache.getInstance().getIngressName(NAMESPACE,
 SERVICE_NAME).isEmpty());
+    }
+
+    /**
+     * test reconcile the deletion of an ingress without annotations.
+     */
+    @Test
+    public void testReconcileDeletedIngressWithoutAnnotations() {
+        ingressReconciler.reconcile(new Request(NAMESPACE, INGRESS_NAME));
+        when(ingressIndexer.getByKey(NAMESPACE + "/" + 
INGRESS_NAME)).thenReturn(null);
+        
when(shenyuCacheRepository.findRuleDataList(anyString())).thenReturn(Collections.emptyList());
+
+        Result result = Assertions.assertDoesNotThrow(
+                () -> ingressReconciler.reconcile(new Request(NAMESPACE, 
INGRESS_NAME)));
+
+        Assertions.assertEquals(new Result(false), result);
+        Assertions.assertNull(IngressCache.getInstance().get(NAMESPACE, 
INGRESS_NAME));
+        
Assertions.assertNull(IngressSelectorCache.getInstance().get(NAMESPACE, 
INGRESS_NAME, PluginEnum.DIVIDE.getName()));
+        
Assertions.assertTrue(ServiceIngressCache.getInstance().getIngressName(NAMESPACE,
 SERVICE_NAME).isEmpty());
+        
verify(shenyuCacheRepository).deleteSelectorData(eq(PluginEnum.DIVIDE.getName()),
 anyString());
+    }
+}

Reply via email to