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

Reply via email to