This is an automated email from the ASF dual-hosted git repository.
dengliming 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 824bcf5ebc fix: prevent ArrayIndexOutOfBoundsException in K8s ingress
protocol annotation parsing (#6892)
824bcf5ebc is described below
commit 824bcf5ebc2794cc5f85676f99e31ed0e501b8fd
Author: wy471x <[email protected]>
AuthorDate: Tue Sep 15 22:39:30 2026 +0800
fix: prevent ArrayIndexOutOfBoundsException in K8s ingress protocol
annotation parsing (#6892)
- DivideIngressParser: add bounds check when indexing protocol array, fall
back to "http://" when annotation has fewer entries than endpoint addresses
- DubboIngressParser: fix NPE when annotation is missing, fix
double-increment bug where protocols[i++] was evaluated twice in one expression
- Add unit tests covering missing, exact, fewer, mixed, and empty
annotation scenarios
Co-authored-by: Claude Opus 4.7 <[email protected]>
Co-authored-by: zhengpeng <[email protected]>
Co-authored-by: Liming Deng <[email protected]>
---
.../shenyu/k8s/parser/DivideIngressParser.java | 7 +-
.../shenyu/k8s/parser/DubboIngressParser.java | 12 +-
.../shenyu/k8s/parser/DivideIngressParserTest.java | 153 ++++++++++++++++++++
.../shenyu/k8s/parser/DubboIngressParserTest.java | 155 +++++++++++++++++++++
4 files changed, 320 insertions(+), 7 deletions(-)
diff --git
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DivideIngressParser.java
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DivideIngressParser.java
index ac5b287eac..d32aebaa97 100644
---
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DivideIngressParser.java
+++
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/DivideIngressParser.java
@@ -314,15 +314,16 @@ public class DivideIngressParser implements
K8sResourceParser<V1Ingress> {
if (Objects.isNull(addresses) || addresses.isEmpty()) {
continue;
}
- int i = 0;
- for (V1EndpointAddress address : addresses) {
+ for (int i = 0; i < addresses.size(); i++) {
+ V1EndpointAddress address = addresses.get(i);
String upstreamIp = address.getIp();
String defaultPort = parsePort(backend.getService());
if (Objects.nonNull(defaultPort)) {
+ String upstreamProtocol = Objects.isNull(protocol)
|| i >= protocol.length ? "http://" : protocol[i];
DivideUpstream upstream = new DivideUpstream();
upstream.setUpstreamUrl(upstreamIp + ":" +
defaultPort);
upstream.setWeight(100);
- upstream.setProtocol(Objects.isNull(protocol) ?
"http://" : protocol[i++]);
+ upstream.setProtocol(upstreamProtocol);
upstream.setWarmup(0);
upstream.setStatus(true);
upstream.setUpstreamHost("");
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 7c76d9c939..4a2e077dcd 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
@@ -336,7 +336,10 @@ public class DubboIngressParser implements
K8sResourceParser<V1Ingress> {
String serviceName = path.getBackend().getService().getName();
V1Endpoints v1Endpoints =
endpointsLister.namespace(namespace).get(serviceName);
List<V1EndpointSubset> subsets = v1Endpoints.getSubsets();
- String[] protocols =
annotations.get(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY).split(",");
+ String[] protocols = null;
+ if (Objects.nonNull(annotations) &&
annotations.containsKey(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY)) {
+ protocols =
annotations.get(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY).split(",");
+ }
if (Objects.isNull(subsets) || CollectionUtils.isEmpty(subsets)) {
LOG.info("Endpoints {} do not have subsets", serviceName);
} else {
@@ -345,15 +348,16 @@ public class DubboIngressParser implements
K8sResourceParser<V1Ingress> {
if (Objects.isNull(addresses) || addresses.isEmpty()) {
continue;
}
- int i = 0;
- for (V1EndpointAddress address : addresses) {
+ for (int i = 0; i < addresses.size(); i++) {
+ V1EndpointAddress address = addresses.get(i);
String upstreamIp = address.getIp();
String defaultPort =
parsePort(path.getBackend().getService());
if (Objects.nonNull(defaultPort)) {
+ String upstreamProtocol =
Objects.isNull(protocols) || i >= protocols.length ? "dubbo://" : protocols[i];
DubboUpstream upstream = DubboUpstream.builder()
.upstreamUrl(upstreamIp + ":" +
defaultPort)
.weight(100)
- .protocol(Objects.isNull(protocols[i++]) ?
"dubbo://" : protocols[i++])
+ .protocol(upstreamProtocol)
.warmup(0)
.status(true)
.upstreamHost("")
diff --git
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DivideIngressParserTest.java
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DivideIngressParserTest.java
new file mode 100644
index 0000000000..aad81bb842
--- /dev/null
+++
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DivideIngressParserTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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 io.kubernetes.client.informer.cache.Indexer;
+import io.kubernetes.client.informer.cache.Lister;
+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.V1IngressRuleBuilder;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.shenyu.common.dto.convert.selector.DivideUpstream;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.common.IngressConstants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for DivideIngressParser upstream protocol parsing.
+ */
+public class DivideIngressParserTest {
+
+ private Lister<V1Service> serviceLister;
+
+ private Indexer<V1Endpoints> endpointsIndexer;
+
+ private Lister<V1Endpoints> endpointsLister;
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ serviceLister = new Lister<>(mock(Indexer.class));
+ endpointsIndexer = mock(Indexer.class);
+ endpointsLister = new Lister<>(endpointsIndexer);
+ }
+
+ private List<DivideUpstream> parseAndGetUpstreams(final Map<String,
String> annotations) {
+ V1Endpoints endpoints = new V1EndpointsBuilder()
+
.withNewMetadata().withNamespace("test").withName("testService").endMetadata()
+ .withSubsets(new V1EndpointSubsetBuilder()
+ .withAddresses(new V1EndpointAddress().ip("10.0.0.1"),
+ new V1EndpointAddress().ip("10.0.0.2"),
+ new V1EndpointAddress().ip("10.0.0.3"))
+ .build())
+ .build();
+
when(endpointsIndexer.getByKey("test/testService")).thenReturn(endpoints);
+
+ Map<String, String> allAnnotations = new HashMap<>();
+ allAnnotations.put("kubernetes.io/ingress.class", "shenyu");
+ if (Objects.nonNull(annotations)) {
+ allAnnotations.putAll(annotations);
+ }
+
+ V1Ingress ingress = new V1IngressBuilder()
+
.withNewMetadata().withName("testIngress").withNamespace("test").withAnnotations(allAnnotations).endMetadata()
+ .withNewSpec().withRules(
+ new V1IngressRuleBuilder().withNewHttp().withPaths(
+ new
V1HTTPIngressPathBuilder().withPath("/test")
+ .withNewBackend()
+
.withNewService().withName("testService").withNewPort().withNumber(8080).endPort().endService()
+ .endBackend().build())
+ .endHttp().build())
+ .endSpec()
+ .build();
+
+ DivideIngressParser parser = new DivideIngressParser(serviceLister,
endpointsLister);
+ ShenyuMemoryConfig result = parser.parse(ingress, null);
+
+ String handle =
result.getRouteConfigList().get(0).getSelectorData().getHandle();
+ return GsonUtils.getInstance().fromList(handle, DivideUpstream.class);
+ }
+
+ @Test
+ public void testProtocolAnnotationMissing() {
+ List<DivideUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(null));
+ assertEquals(3, upstreams.size());
+ for (DivideUpstream upstream : upstreams) {
+ assertEquals("http://", upstream.getProtocol());
+ }
+ }
+
+ @Test
+ public void testProtocolAnnotationExactMatch() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"https://,https://,https://");
+ List<DivideUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ for (DivideUpstream upstream : upstreams) {
+ assertEquals("https://", upstream.getProtocol());
+ }
+ }
+
+ @Test
+ public void testProtocolAnnotationFewerThanAddresses() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"https://");
+ List<DivideUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ assertEquals("https://", upstreams.get(0).getProtocol());
+ assertEquals("http://", upstreams.get(1).getProtocol());
+ assertEquals("http://", upstreams.get(2).getProtocol());
+ }
+
+ @Test
+ public void testProtocolAnnotationMixed() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"https://,http://");
+ List<DivideUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ assertEquals("https://", upstreams.get(0).getProtocol());
+ assertEquals("http://", upstreams.get(1).getProtocol());
+ assertEquals("http://", upstreams.get(2).getProtocol());
+ }
+
+ @Test
+ public void testEmptyProtocolAnnotation() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"");
+ List<DivideUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertNotNull(upstreams);
+ }
+}
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
new file mode 100644
index 0000000000..b53291f074
--- /dev/null
+++
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/DubboIngressParserTest.java
@@ -0,0 +1,155 @@
+/*
+ * 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 io.kubernetes.client.informer.cache.Indexer;
+import io.kubernetes.client.informer.cache.Lister;
+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.V1IngressRuleBuilder;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.shenyu.common.dto.convert.selector.DubboUpstream;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.common.IngressConstants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for DubboIngressParser upstream protocol parsing.
+ */
+public class DubboIngressParserTest {
+
+ private Lister<V1Service> serviceLister;
+
+ private Indexer<V1Endpoints> endpointsIndexer;
+
+ private Lister<V1Endpoints> endpointsLister;
+
+ @BeforeEach
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ serviceLister = new Lister<>(mock(Indexer.class));
+ endpointsIndexer = mock(Indexer.class);
+ endpointsLister = new Lister<>(endpointsIndexer);
+ }
+
+ private List<DubboUpstream> parseAndGetUpstreams(final Map<String, String>
annotations) {
+ V1Endpoints endpoints = new V1EndpointsBuilder()
+
.withNewMetadata().withNamespace("test").withName("testService").endMetadata()
+ .withSubsets(new V1EndpointSubsetBuilder()
+ .withAddresses(new V1EndpointAddress().ip("10.0.0.1"),
+ new V1EndpointAddress().ip("10.0.0.2"),
+ new V1EndpointAddress().ip("10.0.0.3"))
+ .build())
+ .build();
+
when(endpointsIndexer.getByKey("test/testService")).thenReturn(endpoints);
+
+ Map<String, String> allAnnotations = new HashMap<>();
+ allAnnotations.put("kubernetes.io/ingress.class", "shenyu");
+ if (Objects.nonNull(annotations)) {
+ allAnnotations.putAll(annotations);
+ }
+ Map<String, String> labels = new HashMap<>();
+
+ V1Ingress ingress = new V1IngressBuilder()
+
.withNewMetadata().withName("testIngress").withNamespace("test")
+
.withAnnotations(allAnnotations).withLabels(labels).endMetadata()
+ .withNewSpec().withRules(
+ new V1IngressRuleBuilder().withNewHttp().withPaths(
+ new
V1HTTPIngressPathBuilder().withPath("/test")
+ .withNewBackend()
+
.withNewService().withName("testService").withNewPort().withNumber(20880).endPort().endService()
+ .endBackend().build())
+ .endHttp().build())
+ .endSpec()
+ .build();
+
+ DubboIngressParser parser = new DubboIngressParser(serviceLister,
endpointsLister);
+ ShenyuMemoryConfig result = parser.parse(ingress, null);
+
+ String handle =
result.getRouteConfigList().get(0).getSelectorData().getHandle();
+ return GsonUtils.getInstance().fromList(handle, DubboUpstream.class);
+ }
+
+ @Test
+ public void testProtocolAnnotationMissing() {
+ List<DubboUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(null));
+ assertEquals(3, upstreams.size());
+ for (DubboUpstream upstream : upstreams) {
+ assertEquals("dubbo://", upstream.getProtocol());
+ }
+ }
+
+ @Test
+ public void testProtocolAnnotationExactMatch() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"dubbo://,dubbo://,dubbo://");
+ List<DubboUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ for (DubboUpstream upstream : upstreams) {
+ assertEquals("dubbo://", upstream.getProtocol());
+ }
+ }
+
+ @Test
+ public void testProtocolAnnotationFewerThanAddresses() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"triple://");
+ List<DubboUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ assertEquals("triple://", upstreams.get(0).getProtocol());
+ assertEquals("dubbo://", upstreams.get(1).getProtocol());
+ assertEquals("dubbo://", upstreams.get(2).getProtocol());
+ }
+
+ @Test
+ public void testProtocolAnnotationMixed() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"triple://,dubbo://");
+ List<DubboUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertEquals(3, upstreams.size());
+ assertEquals("triple://", upstreams.get(0).getProtocol());
+ assertEquals("dubbo://", upstreams.get(1).getProtocol());
+ assertEquals("dubbo://", upstreams.get(2).getProtocol());
+ }
+
+ @Test
+ public void testEmptyProtocolAnnotation() {
+ Map<String, String> annotations = new HashMap<>();
+ annotations.put(IngressConstants.UPSTREAMS_PROTOCOL_ANNOTATION_KEY,
"");
+ List<DubboUpstream> upstreams = assertDoesNotThrow(() ->
parseAndGetUpstreams(annotations));
+ assertNotNull(upstreams);
+ }
+}