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 e2720f7fb6 fix: resolve context path annotations from the referenced
service (#6492) (#7286)
e2720f7fb6 is described below
commit e2720f7fb66acd59b508bc54b495d356bf47f031
Author: wy471x <[email protected]>
AuthorDate: Sat Sep 26 11:18:32 2026 +0800
fix: resolve context path annotations from the referenced service (#6492)
(#7286)
---
.../shenyu/k8s/parser/ContextPathParser.java | 66 ++++++++-
.../parser/ContextPathParserAnnotationsTest.java | 156 +++++++++++++++++++++
2 files changed, 221 insertions(+), 1 deletion(-)
diff --git
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/ContextPathParser.java
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/ContextPathParser.java
index e6a15cfad2..555096eb78 100644
---
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/ContextPathParser.java
+++
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/ContextPathParser.java
@@ -23,7 +23,10 @@ import io.kubernetes.client.openapi.models.V1Endpoints;
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.V1ObjectMeta;
import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.commons.collections4.MapUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.shenyu.common.dto.ConditionData;
import org.apache.shenyu.common.dto.RuleData;
import org.apache.shenyu.common.dto.SelectorData;
@@ -41,6 +44,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -52,6 +58,11 @@ public class ContextPathParser implements
K8sResourceParser<V1Ingress> {
private static final Logger LOG =
LoggerFactory.getLogger(ContextPathParser.class);
+ private static final List<String> CONTEXT_PATH_ANNOTATION_KEYS =
Arrays.asList(
+ IngressConstants.PLUGIN_CONTEXT_PATH_PATH,
+ IngressConstants.PLUGIN_CONTEXT_PATH_ADD_PREFIX,
+ IngressConstants.PLUGIN_CONTEXT_PATH_ADD_PREFIXED);
+
private final Lister<V1Service> serviceLister;
private final Lister<V1Endpoints> endpointsLister;
@@ -82,9 +93,10 @@ public class ContextPathParser implements
K8sResourceParser<V1Ingress> {
// if rules is null, context path Plugin will not execute the
List<V1IngressRule> rules = ingress.getSpec().getRules();
if (Objects.nonNull(rules)) {
+ Map<String, String> annotations =
resolveContextPathAnnotations(ingress);
List<IngressConfiguration> routeList = new
ArrayList<>(rules.size());
for (V1IngressRule ingressRule : rules) {
- List<IngressConfiguration> routes =
parseIngressRule(ingressRule, ingress.getMetadata().getAnnotations());
+ List<IngressConfiguration> routes =
parseIngressRule(ingressRule, annotations);
routeList.addAll(routes);
}
res.setRouteConfigList(routeList);
@@ -104,6 +116,10 @@ public class ContextPathParser implements
K8sResourceParser<V1Ingress> {
if (Objects.isNull(pathPath)) {
continue;
}
+ if
(StringUtils.isBlank(annotations.get(IngressConstants.PLUGIN_CONTEXT_PATH_PATH)))
{
+ LOG.info("Cannot find context path annotation for {},
skip it", pathPath);
+ continue;
+ }
OperatorEnum operator = getOperator(path.getPathType());
ConditionData pathCondition =
createPathCondition(pathPath, operator);
List<ConditionData> conditionList = new ArrayList<>(2);
@@ -124,6 +140,54 @@ public class ContextPathParser implements
K8sResourceParser<V1Ingress> {
return res;
}
+ /**
+ * Resolve the context path annotations used to build the context mapping
rule. An RPC ingress
+ * declares them on the metadata service that is referenced by the ingress
labels, the same
+ * resource the RPC parsers read their annotations from, so the service
annotations are used
+ * when the ingress does not declare them itself.
+ *
+ * @param ingress ingress resource
+ * @return the resolved context path annotations, empty if neither the
ingress nor the
+ * referenced services declare them
+ */
+ private Map<String, String> resolveContextPathAnnotations(final V1Ingress
ingress) {
+ if (Objects.isNull(ingress.getMetadata())) {
+ return Collections.emptyMap();
+ }
+ V1ObjectMeta metadata = ingress.getMetadata();
+ Map<String, String> serviceAnnotations =
getReferencedServiceAnnotations(metadata.getNamespace(), metadata.getLabels());
+ Map<String, String> ingressAnnotations =
MapUtils.emptyIfNull(metadata.getAnnotations());
+ Map<String, String> res = new
HashMap<>(CONTEXT_PATH_ANNOTATION_KEYS.size());
+ for (String key : CONTEXT_PATH_ANNOTATION_KEYS) {
+ String value = StringUtils.isNotBlank(ingressAnnotations.get(key))
? ingressAnnotations.get(key) : serviceAnnotations.get(key);
+ if (Objects.nonNull(value)) {
+ res.put(key, value);
+ }
+ }
+ return res;
+ }
+
+ private Map<String, String> getReferencedServiceAnnotations(final String
namespace, final Map<String, String> labels) {
+ if (MapUtils.isEmpty(labels)) {
+ return Collections.emptyMap();
+ }
+ Map<String, String> res = new HashMap<>(labels.size());
+ for (String serviceName : labels.values()) {
+ V1Service service =
serviceLister.namespace(namespace).get(serviceName);
+ if (Objects.isNull(service) ||
Objects.isNull(service.getMetadata())) {
+ continue;
+ }
+ Map<String, String> annotations =
MapUtils.emptyIfNull(service.getMetadata().getAnnotations());
+ for (String key : CONTEXT_PATH_ANNOTATION_KEYS) {
+ String value = annotations.get(key);
+ if (Objects.nonNull(value)) {
+ res.putIfAbsent(key, value);
+ }
+ }
+ }
+ return res;
+ }
+
private ConditionData createHostCondition(final String host) {
ConditionData hostCondition = new ConditionData();
hostCondition.setParamType(ParamTypeEnum.DOMAIN.getName());
diff --git
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/ContextPathParserAnnotationsTest.java
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/ContextPathParserAnnotationsTest.java
new file mode 100644
index 0000000000..e99215f77d
--- /dev/null
+++
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/ContextPathParserAnnotationsTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.apis.CoreV1Api;
+import io.kubernetes.client.openapi.models.V1Endpoints;
+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.V1Service;
+import io.kubernetes.client.openapi.models.V1ServiceBuilder;
+import org.apache.shenyu.common.dto.convert.rule.impl.ContextMappingRuleHandle;
+import org.apache.shenyu.common.utils.GsonUtils;
+import org.apache.shenyu.k8s.common.IngressConfiguration;
+import org.apache.shenyu.k8s.common.IngressConstants;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for resolving the context path annotations of an ingress.
+ */
+public final class ContextPathParserAnnotationsTest {
+
+ private static final String NAMESPACE = "context-path-ns";
+
+ private static final String METADATA_SERVICE =
"context-path-metadata-service";
+
+ private static final String CONTEXT_PATH = "/grpc";
+
+ private Indexer<V1Service> serviceIndexer;
+
+ private ContextPathParser contextPathParser;
+
+ @BeforeEach
+ public void init() {
+ serviceIndexer = mock(Indexer.class);
+ Indexer<V1Endpoints> endpointsIndexer = mock(Indexer.class);
+ contextPathParser = new ContextPathParser(new
Lister<>(serviceIndexer), new Lister<>(endpointsIndexer));
+ }
+
+ /**
+ * test parse a Prefix path ingress that does not declare the context path
annotation.
+ */
+ @Test
+ public void testParseWithoutContextPathAnnotation() {
+ ShenyuMemoryConfig config = contextPathParser.parse(buildIngress(null,
null), mock(CoreV1Api.class));
+
+ Assertions.assertNotNull(config.getRouteConfigList());
+ Assertions.assertTrue(config.getRouteConfigList().isEmpty());
+ }
+
+ /**
+ * test parse the context path annotation from the metadata service of the
ingress labels.
+ */
+ @Test
+ public void testParseContextPathFromMetadataService() {
+ mockMetadataService(CONTEXT_PATH);
+ Map<String, String> labels =
Collections.singletonMap("shenyu.apache.org/metadata-labels-1",
METADATA_SERVICE);
+
+ ShenyuMemoryConfig config = contextPathParser.parse(buildIngress(null,
labels), mock(CoreV1Api.class));
+
+ List<IngressConfiguration> routeConfigList =
config.getRouteConfigList();
+ Assertions.assertEquals(1, routeConfigList.size());
+ ContextMappingRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(
+ routeConfigList.get(0).getRuleDataList().get(0).getHandle(),
ContextMappingRuleHandle.class);
+ Assertions.assertEquals(CONTEXT_PATH, ruleHandle.getContextPath());
+ Assertions.assertEquals(CONTEXT_PATH,
routeConfigList.get(0).getRuleDataList().get(0).getName());
+ Assertions.assertEquals(CONTEXT_PATH + "/**",
+
routeConfigList.get(0).getRuleDataList().get(0).getConditionDataList().get(0).getParamValue());
+ }
+
+ /**
+ * test parse an ingress that declares the context path annotation itself.
+ */
+ @Test
+ public void testIngressAnnotationWins() {
+ mockMetadataService("/sofa");
+ Map<String, String> annotations =
Collections.singletonMap(IngressConstants.PLUGIN_CONTEXT_PATH_PATH,
CONTEXT_PATH);
+ Map<String, String> labels =
Collections.singletonMap("shenyu.apache.org/metadata-labels-1",
METADATA_SERVICE);
+
+ ShenyuMemoryConfig config =
contextPathParser.parse(buildIngress(annotations, labels),
mock(CoreV1Api.class));
+
+ List<IngressConfiguration> routeConfigList =
config.getRouteConfigList();
+ Assertions.assertEquals(1, routeConfigList.size());
+ ContextMappingRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(
+ routeConfigList.get(0).getRuleDataList().get(0).getHandle(),
ContextMappingRuleHandle.class);
+ Assertions.assertEquals(CONTEXT_PATH, ruleHandle.getContextPath());
+ }
+
+ /**
+ * test parse an ingress that references labels which are not registered
services.
+ */
+ @Test
+ public void testParseWithUnknownLabelServices() {
+ Map<String, String> labels = new HashMap<>();
+ labels.put("app", "shenyu-examples-grpc-service");
+ labels.put("shenyu.apache.org/metadata-labels-1", METADATA_SERVICE);
+
+ ShenyuMemoryConfig config = contextPathParser.parse(buildIngress(null,
labels), mock(CoreV1Api.class));
+
+ Assertions.assertNotNull(config.getRouteConfigList());
+ Assertions.assertTrue(config.getRouteConfigList().isEmpty());
+ }
+
+ private void mockMetadataService(final String contextPath) {
+ V1Service service = new V1ServiceBuilder().withNewMetadata()
+ .withName(METADATA_SERVICE).withNamespace(NAMESPACE)
+
.withAnnotations(Collections.singletonMap(IngressConstants.PLUGIN_CONTEXT_PATH_PATH,
contextPath)).endMetadata()
+ .withNewSpec().endSpec()
+ .withKind("Service").build();
+ when(serviceIndexer.getByKey(NAMESPACE + "/" +
METADATA_SERVICE)).thenReturn(service);
+ }
+
+ private V1Ingress buildIngress(final Map<String, String> annotations,
final Map<String, String> labels) {
+ V1IngressRule rule = new
V1IngressRuleBuilder().withNewHttp().withPaths(
+ new
V1HTTPIngressPathBuilder().withPath("/grpc/").withPathType("Prefix")
+ .withNewBackend()
+
.withNewService().withName("shenyu-examples-grpc-service").withNewPort().withNumber(38080).endPort().endService()
+ .endBackend().build())
+ .endHttp().build();
+ return new V1IngressBuilder().withNewMetadata()
+
.withName("context-path-ingress").withNamespace(NAMESPACE)
+
.withAnnotations(annotations).withLabels(labels).endMetadata()
+ .withNewSpec().withRules(rule).endSpec()
+ .withKind("Ingress").build();
+ }
+}