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());
+ }
+}