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 91bd8d0c18 [type:refactor] enable external ingress adapters for Motan 
asset handoff (#7498)
91bd8d0c18 is described below

commit 91bd8d0c18fae8a1b297a3a752ab3282643bc7f9
Author: cloudbase-ai <[email protected]>
AuthorDate: Fri Oct 9 12:06:07 2026 +0800

    [type:refactor] enable external ingress adapters for Motan asset handoff 
(#7498)
    
    * chore: remove stale Motan distribution ownership metadata
    
    Signed-off-by: cloudbase-ai <[email protected]>
    
    * feat: support external ingress plugin definitions without core coupling
    
    Signed-off-by: cloudbase-ai <[email protected]>
    
    ---------
    
    Signed-off-by: cloudbase-ai <[email protected]>
    Co-authored-by: cloudbase-ai 
<[email protected]>
---
 .../src/main/release-docs/LICENSE                  |  4 -
 .../main/release-docs/licenses/LICENSE-motan.txt   | 16 ----
 .../apache/shenyu/k8s/parser/IngressParser.java    | 45 +++++++++-
 .../shenyu/k8s/parser/IngressPluginDefinition.java | 99 ++++++++++++++++++++++
 .../shenyu/k8s/reconciler/IngressReconciler.java   | 40 +++++++--
 .../apache/shenyu/k8s/IngressReconcilerTest.java   | 81 ++++++++++++++++++
 .../shenyu/k8s/parser/IngressParserTest.java       | 57 +++++++++++++
 .../k8s/IngressControllerConfiguration.java        | 11 ++-
 8 files changed, 322 insertions(+), 31 deletions(-)

diff --git a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
index ae16c477a8..11374b4279 100644
--- a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
+++ b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/LICENSE
@@ -635,10 +635,6 @@ The text of each license is the standard Apache 2.0 
license.
     jsonschema-module-jackson 4.38.0: 
https://github.com/victools/jsonschema-generator, Apache 2.0
     jsonschema-module-swagger-2 4.38.0: 
https://github.com/victools/jsonschema-generator, Apache 2.0
     json-schema-validator 2.0.0: 
https://github.com/networknt/json-schema-validator, Apache 2.0
-    motan-core 1.2.1: https://github.com/weibocom/motan, Apache 2.0
-    motan-registry-zookeeper 1.2.1: https://github.com/weibocom/motan, Apache 
2.0
-    motan-springsupport 1.2.1: https://github.com/weibocom/motan, Apache 2.0
-    motan-transport-netty4 1.2.1: https://github.com/weibocom/motan, Apache 2.0
     swagger-annotations-jakarta 2.2.38: 
https://github.com/swagger-api/swagger-core, Apache 2.0
     dubbo-dependencies-zookeeper 3.2.14: https://github.com/apache/dubbo, 
Apache 2.0
     kafka-clients 3.9.2: https://kafka.apache.org, Apache 2.0
diff --git 
a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-motan.txt
 
b/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-motan.txt
deleted file mode 100644
index 6ebeddc98d..0000000000
--- 
a/shenyu-dist/shenyu-bootstrap-dist/src/main/release-docs/licenses/LICENSE-motan.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-motan License
----------------
-
-Copyright 2009-2016 Weibo, Inc.
-
-All files licensed under the Apache License, Version 2.0 (the "License");
-you may not use these files 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.
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 c4cdbcc663..40ac04b8ec 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
@@ -32,9 +32,11 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Parser of Ingress.
@@ -47,6 +49,8 @@ public class IngressParser implements 
K8sResourceListParser<V1Ingress> {
 
     private final Lister<V1Endpoints> endpointsLister;
 
+    private final List<IngressPluginDefinition> pluginDefinitions;
+
     /**
      * IngressParser Constructor.
      *
@@ -54,8 +58,21 @@ public class IngressParser implements 
K8sResourceListParser<V1Ingress> {
      * @param endpointsInformer endpointsInformer
      */
     public IngressParser(final SharedIndexInformer<V1Service> serviceInformer, 
final SharedIndexInformer<V1Endpoints> endpointsInformer) {
+        this(serviceInformer, endpointsInformer, Collections.emptyList());
+    }
+
+    /**
+     * IngressParser Constructor.
+     *
+     * @param serviceInformer   serviceInformer
+     * @param endpointsInformer endpointsInformer
+     * @param pluginDefinitions external plugin definitions
+     */
+    public IngressParser(final SharedIndexInformer<V1Service> serviceInformer, 
final SharedIndexInformer<V1Endpoints> endpointsInformer,
+                         final List<IngressPluginDefinition> 
pluginDefinitions) {
         this.serviceLister = new Lister<>(serviceInformer.getIndexer());
         this.endpointsLister = new Lister<>(endpointsInformer.getIndexer());
+        this.pluginDefinitions = Objects.isNull(pluginDefinitions) ? 
Collections.emptyList() : new ArrayList<>(pluginDefinitions);
     }
 
     /**
@@ -73,11 +90,14 @@ public class IngressParser implements 
K8sResourceListParser<V1Ingress> {
         boolean brpcEnabled = getBooleanAnnotation(ingress, 
IngressConstants.PLUGIN_BRPC_ENABLED);
         boolean grpcEnabled = getBooleanAnnotation(ingress, 
IngressConstants.PLUGIN_GRPC_ENABLED);
         boolean sofaEnabled = getBooleanAnnotation(ingress, 
IngressConstants.PLUGIN_SOFA_ENABLED);
+        Optional<IngressPluginDefinition> pluginDefinition = 
findPluginDefinition(ingress);
 
-        if (!dubboEnabled && !webSocketEnabled && !brpcEnabled && !grpcEnabled 
&& !sofaEnabled) {
+        if (!dubboEnabled && !webSocketEnabled && !brpcEnabled && !grpcEnabled 
&& !sofaEnabled && !pluginDefinition.isPresent()) {
             contextPathParse(ingress, shenyuMemoryConfigList, coreV1Api);
         }
-        if (dubboEnabled) {
+        if (pluginDefinition.isPresent()) {
+            shenyuMemoryConfigList.add(pluginDefinition.get().parse(ingress, 
coreV1Api, serviceLister, endpointsLister));
+        } else if (dubboEnabled) {
             DubboIngressParser dubboIngressParser = new 
DubboIngressParser(serviceLister, endpointsLister);
             shenyuMemoryConfigList.add(dubboIngressParser.parse(ingress, 
coreV1Api));
         } else if (webSocketEnabled) {
@@ -96,6 +116,27 @@ public class IngressParser implements 
K8sResourceListParser<V1Ingress> {
         return shenyuMemoryConfigList;
     }
 
+    /**
+     * Find external plugin definition for the ingress.
+     *
+     * @param ingress ingress
+     * @return first matching definition
+     */
+    public Optional<IngressPluginDefinition> findPluginDefinition(final 
V1Ingress ingress) {
+        return pluginDefinitions.stream().filter(definition -> 
definition.matchesIngress(ingress)).findFirst();
+    }
+
+    /**
+     * Find external plugin-owned metadata paths for the ingress.
+     *
+     * @param definition plugin definition
+     * @param ingress ingress
+     * @return metadata paths owned by the definition
+     */
+    public List<String> findMetadataPaths(final IngressPluginDefinition 
definition, final V1Ingress ingress) {
+        return definition.metadataPaths(ingress, serviceLister, 
endpointsLister);
+    }
+
     private boolean getBooleanAnnotation(final V1Ingress ingress, final String 
annotationKey) {
         Map<String, String> annotations = 
Objects.isNull(ingress.getMetadata()) ? null : 
ingress.getMetadata().getAnnotations();
         String annotationValue = 
MapUtils.emptyIfNull(annotations).get(annotationKey);
diff --git 
a/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressPluginDefinition.java
 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressPluginDefinition.java
new file mode 100644
index 0000000000..97d5ed4312
--- /dev/null
+++ 
b/shenyu-kubernetes-controller/src/main/java/org/apache/shenyu/k8s/parser/IngressPluginDefinition.java
@@ -0,0 +1,99 @@
+/*
+ * 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.extended.controller.reconciler.Request;
+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.V1Ingress;
+import io.kubernetes.client.openapi.models.V1Service;
+import org.apache.shenyu.common.dto.PluginData;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * External ingress plugin definition.
+ *
+ * <p>When more than one external definition matches an ingress, the first
+ * definition in the injected list wins. Spring wiring uses ordered stream
+ * semantics, so extensions can use {@code @Order} to make overlap 
deterministic.
+ */
+public interface IngressPluginDefinition {
+
+    /**
+     * Whether this definition owns the ingress.
+     *
+     * @param ingress ingress
+     * @return true when this definition owns the ingress
+     */
+    boolean matchesIngress(V1Ingress ingress);
+
+    /**
+     * Plugin name used for selector cache and repository operations.
+     *
+     * @return plugin name
+     */
+    String pluginName();
+
+    /**
+     * Metadata path used when deleting metadata for selectors.
+     *
+     * @param ingress ingress
+     * @return metadata path, or empty when no metadata deletion path is needed
+     */
+    String contextPath(V1Ingress ingress);
+
+    /**
+     * Metadata paths owned by this ingress definition.
+     *
+     * @param ingress ingress
+     * @param serviceLister service lister
+     * @param endpointsLister endpoints lister
+     * @return metadata paths owned by this ingress
+     */
+    default List<String> metadataPaths(final V1Ingress ingress, final 
Lister<V1Service> serviceLister,
+                                       final Lister<V1Endpoints> 
endpointsLister) {
+        String path = contextPath(ingress);
+        return Objects.isNull(path) || path.isEmpty() ? 
Collections.emptyList() : Collections.singletonList(path);
+    }
+
+    /**
+     * Build plugin data for this ingress.
+     *
+     * @param ingress ingress
+     * @param request reconcile request
+     * @param endpointsLister endpoints lister
+     * @return plugin data
+     */
+    PluginData pluginData(V1Ingress ingress, Request request, 
Lister<V1Endpoints> endpointsLister);
+
+    /**
+     * Parse ingress config for this plugin.
+     *
+     * @param ingress ingress
+     * @param coreV1Api core v1 api
+     * @param serviceLister service lister
+     * @param endpointsLister endpoints lister
+     * @return memory config
+     */
+    ShenyuMemoryConfig parse(V1Ingress ingress, CoreV1Api coreV1Api, 
Lister<V1Service> serviceLister, Lister<V1Endpoints> endpointsLister);
+}
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 7d2a91bec8..fb51ab847c 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
@@ -60,6 +60,7 @@ import org.apache.shenyu.k8s.common.IngressConstants;
 import org.apache.shenyu.k8s.common.IngressUtils;
 import org.apache.shenyu.k8s.common.ServiceIngressRelation;
 import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.IngressPluginDefinition;
 import org.apache.shenyu.k8s.parser.IngressParser;
 import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
 import org.slf4j.Logger;
@@ -70,6 +71,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -166,7 +168,7 @@ public class IngressReconciler implements Reconciler {
             return new Result(false);
         }
         Map<String, String> annotations = getAnnotations(v1Ingress);
-        enablePluginsBasedOnAnnotations(annotations, request);
+        enablePluginsBasedOnAnnotations(v1Ingress, annotations, request);
 
         if (!checkIngressClass(v1Ingress)) {
             LOG.info("IngressClass is not match {}", request);
@@ -208,8 +210,14 @@ public class IngressReconciler implements Reconciler {
         return new Result(false);
     }
 
-    private void enablePluginsBasedOnAnnotations(final Map<String, String> 
annotations, final Request request) {
-        if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
+    private void enablePluginsBasedOnAnnotations(final V1Ingress ingress, 
final Map<String, String> annotations, final Request request) {
+        Optional<IngressPluginDefinition> pluginDefinition = 
ingressParser.findPluginDefinition(ingress);
+        if (pluginDefinition.isPresent()) {
+            PluginData pluginData = pluginDefinition.get().pluginData(ingress, 
request, ingressParser.getEndpointsLister());
+            if (Objects.nonNull(pluginData)) {
+                shenyuCacheRepository.saveOrUpdatePluginData(pluginData);
+            }
+        } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
             String zookeeperUrl = getZookeeperUrl(annotations, request);
             enablePlugin(shenyuCacheRepository, PluginEnum.DUBBO, 
zookeeperUrl);
         } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED), 
"true")) {
@@ -225,7 +233,12 @@ 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(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
+        Optional<IngressPluginDefinition> pluginDefinition = 
ingressParser.findPluginDefinition(oldIngress);
+        if (pluginDefinition.isPresent()) {
+            List<String> metadataPaths = 
ingressParser.findMetadataPaths(pluginDefinition.get(), oldIngress);
+            selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), pluginDefinition.get().pluginName(),
+                    metadataPaths);
+        } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.DUBBO.getName(),
                     
annotations.get(IngressConstants.PLUGIN_DUBBO_CONTEXT_PATH));
         } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED), 
"true")) {
@@ -239,7 +252,9 @@ public class IngressReconciler implements Reconciler {
             selectorList = deleteSelectorByIngressName(request.getNamespace(), 
request.getName(), PluginEnum.DIVIDE.getName(), "");
         }
         if (Objects.nonNull(selectorList) && !selectorList.isEmpty()) {
-            if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
+            if (pluginDefinition.isPresent()) {
+                
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), pluginDefinition.get().pluginName());
+            } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED), 
"true")) {
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.DUBBO.getName());
             } else if 
(Objects.equals(annotations.get(IngressConstants.PLUGIN_WEB_SOCKET_ENABLED), 
"true")) {
                 
IngressSelectorCache.getInstance().remove(request.getNamespace(), 
request.getName(), PluginEnum.WEB_SOCKET.getName());
@@ -369,18 +384,27 @@ public class IngressReconciler implements Reconciler {
 
     private List<String> deleteSelectorByIngressName(final String namespace, 
final String name,
                                                      final String pluginName, 
final String path) {
+        List<String> metadataPaths = Objects.isNull(path) || path.isEmpty() ? 
Collections.emptyList() : Collections.singletonList(path);
+        return deleteSelectorByIngressName(namespace, name, pluginName, 
metadataPaths);
+    }
+
+    private List<String> deleteSelectorByIngressName(final String namespace, 
final String name,
+                                                     final String pluginName, 
final List<String> paths) {
         final List<String> selectorList = 
IngressSelectorCache.getInstance().get(namespace, name, pluginName);
         if (Objects.nonNull(selectorList) && !selectorList.isEmpty()) {
+            Set<String> metadataPaths = new 
LinkedHashSet<>(CollectionUtils.emptyIfNull(paths));
             for (String selectorId : selectorList) {
                 List<RuleData> ruleList = 
shenyuCacheRepository.findRuleDataList(selectorId);
                 // To avoid ConcurrentModificationException, copy the ruleId 
to list
                 List<String> ruleIdList = new ArrayList<>();
                 ruleList.forEach(rule -> ruleIdList.add(rule.getId()));
-                for (String id : ruleIdList) {
+                for (String path : metadataPaths) {
                     MetaData metaData = 
shenyuCacheRepository.findMetaData(path);
                     if (Objects.nonNull(metaData)) {
                         shenyuCacheRepository.deleteMetaData(metaData);
                     }
+                }
+                for (String id : ruleIdList) {
                     shenyuCacheRepository.deleteRuleData(pluginName, 
selectorId, id);
                 }
                 shenyuCacheRepository.deleteSelectorData(pluginName, 
selectorId);
@@ -714,6 +738,10 @@ public class IngressReconciler implements Reconciler {
     }
 
     private String getPluginName(final V1Ingress ingress) {
+        Optional<IngressPluginDefinition> pluginDefinition = 
ingressParser.findPluginDefinition(ingress);
+        if (pluginDefinition.isPresent()) {
+            return pluginDefinition.get().pluginName();
+        }
         Map<String, String> annotations = getAnnotations(ingress);
         String pluginName;
         String pluginDubboEnabled = 
annotations.get(IngressConstants.PLUGIN_DUBBO_ENABLED);
diff --git 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerTest.java
 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerTest.java
index 15bd299f13..2471a635d2 100644
--- 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerTest.java
+++ 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/IngressReconcilerTest.java
@@ -21,7 +21,9 @@ 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.informer.cache.Lister;
 import io.kubernetes.client.openapi.ApiClient;
+import io.kubernetes.client.openapi.apis.CoreV1Api;
 import io.kubernetes.client.openapi.models.CoreV1EndpointPort;
 import io.kubernetes.client.openapi.models.V1EndpointAddress;
 import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
@@ -36,13 +38,18 @@ 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.dto.PluginData;
+import org.apache.shenyu.common.dto.RuleData;
 import org.apache.shenyu.common.dto.SelectorData;
 import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
 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.common.IngressConfiguration;
 import org.apache.shenyu.k8s.common.IngressConstants;
 import org.apache.shenyu.k8s.common.ServiceIngressRelation;
+import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
+import org.apache.shenyu.k8s.parser.IngressPluginDefinition;
 import org.apache.shenyu.k8s.parser.IngressParser;
 import org.apache.shenyu.k8s.reconciler.IngressReconciler;
 import org.apache.shenyu.k8s.repository.ShenyuCacheRepository;
@@ -233,6 +240,33 @@ public final class IngressReconcilerTest {
         assertTrue(ServiceIngressCache.getInstance().getIngressName(NAMESPACE, 
SERVICE_NAME).isEmpty());
     }
 
+    @Test
+    @SuppressWarnings("unchecked")
+    public void testExternalPluginReconcileAndDeleteUseExternalPluginName() {
+        IngressParser parser = new IngressParser(mockInformer(), 
mockInformer(), Collections.singletonList(new FakeIngressPluginDefinition()));
+        ingressReconciler = new IngressReconciler(mockIngressInformer(), 
mockInformer(), shenyuCacheRepository,
+                new ShenyuSniAsyncMapping(), parser, mock(ApiClient.class));
+        Map<String, String> annotations = shenyuAnnotations(new HashMap<>());
+        annotations.put("shenyu.apache.org/plugin-fake-enabled", "true");
+        mockIngress(annotations, "/fake", "Exact", null);
+
+        Result result = ingressReconciler.reconcile(new Request(NAMESPACE, 
INGRESS_NAME));
+
+        assertEquals(new Result(false), result);
+        ArgumentCaptor<PluginData> pluginCaptor = 
ArgumentCaptor.forClass(PluginData.class);
+        verify(shenyuCacheRepository, 
atLeastOnce()).saveOrUpdatePluginData(pluginCaptor.capture());
+        assertTrue(pluginCaptor.getAllValues().stream().anyMatch(pluginData -> 
"fake".equals(pluginData.getName())));
+        assertNotNull(IngressSelectorCache.getInstance().get(NAMESPACE, 
INGRESS_NAME, "fake"));
+        assertNull(IngressSelectorCache.getInstance().get(NAMESPACE, 
INGRESS_NAME, PluginEnum.DIVIDE.getName()));
+
+        
when(shenyuCacheRepository.findRuleDataList(anyString())).thenReturn(Collections.singletonList(RuleData.builder().id("rule-id").build()));
+        when(ingressIndexer.getByKey(NAMESPACE + "/" + 
INGRESS_NAME)).thenReturn(null);
+        ingressReconciler.reconcile(new Request(NAMESPACE, INGRESS_NAME));
+
+        verify(shenyuCacheRepository, 
atLeastOnce()).deleteSelectorData(eq("fake"), anyString());
+        assertNull(IngressSelectorCache.getInstance().get(NAMESPACE, 
INGRESS_NAME, "fake"));
+    }
+
     private Map<String, String> shenyuAnnotations(final Map<String, String> 
annotations) {
         Map<String, String> allAnnotations = new HashMap<>();
         allAnnotations.put(IngressConstants.K8S_INGRESS_CLASS_ANNOTATION_KEY, 
IngressConstants.SHENYU_INGRESS_CLASS);
@@ -258,4 +292,51 @@ public final class IngressReconcilerTest {
                 .build();
         when(ingressIndexer.getByKey(NAMESPACE + "/" + 
INGRESS_NAME)).thenReturn(ingress);
     }
+
+    @SuppressWarnings("unchecked")
+    private SharedIndexInformer<V1Ingress> mockIngressInformer() {
+        SharedIndexInformer<V1Ingress> ingressInformer = 
mock(SharedIndexInformer.class);
+        ingressIndexer = mock(Indexer.class);
+        when(ingressInformer.getIndexer()).thenReturn(ingressIndexer);
+        return ingressInformer;
+    }
+
+    @SuppressWarnings("unchecked")
+    private SharedIndexInformer mockInformer() {
+        SharedIndexInformer informer = mock(SharedIndexInformer.class);
+        when(informer.getIndexer()).thenReturn(mock(Indexer.class));
+        return informer;
+    }
+
+    private static final class FakeIngressPluginDefinition implements 
IngressPluginDefinition {
+
+        @Override
+        public boolean matchesIngress(final V1Ingress ingress) {
+            return 
"true".equals(ingress.getMetadata().getAnnotations().get("shenyu.apache.org/plugin-fake-enabled"));
+        }
+
+        @Override
+        public String pluginName() {
+            return "fake";
+        }
+
+        @Override
+        public String contextPath(final V1Ingress ingress) {
+            return "/fake";
+        }
+
+        @Override
+        public PluginData pluginData(final V1Ingress ingress, final Request 
request, final Lister<V1Endpoints> endpointsLister) {
+            return 
PluginData.builder().id("9000").name("fake").config("{}").role(PluginRoleEnum.SYS.getName()).enabled(true).sort(9000).build();
+        }
+
+        @Override
+        public ShenyuMemoryConfig parse(final V1Ingress ingress, final 
CoreV1Api coreV1Api,
+                                        final Lister<V1Service> serviceLister, 
final Lister<V1Endpoints> endpointsLister) {
+            ShenyuMemoryConfig config = new ShenyuMemoryConfig();
+            SelectorData selectorData = 
SelectorData.builder().pluginName("fake").name("fake").enabled(true).build();
+            config.setRouteConfigList(Collections.singletonList(new 
IngressConfiguration(selectorData, Collections.emptyList(), 
Collections.emptyList())));
+            return config;
+        }
+    }
 }
diff --git 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/IngressParserTest.java
 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/IngressParserTest.java
index 707a087359..1ce651bdb8 100644
--- 
a/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/IngressParserTest.java
+++ 
b/shenyu-kubernetes-controller/src/test/java/org/apache/shenyu/k8s/parser/IngressParserTest.java
@@ -17,8 +17,10 @@
 
 package org.apache.shenyu.k8s.parser;
 
+import io.kubernetes.client.extended.controller.reconciler.Request;
 import io.kubernetes.client.informer.SharedIndexInformer;
 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.V1EndpointAddress;
 import io.kubernetes.client.openapi.models.V1EndpointSubsetBuilder;
@@ -30,8 +32,10 @@ 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 org.apache.shenyu.common.dto.PluginData;
 import org.apache.shenyu.common.dto.SelectorData;
 import org.apache.shenyu.common.enums.PluginEnum;
+import org.apache.shenyu.common.enums.PluginRoleEnum;
 import org.apache.shenyu.k8s.common.IngressConfiguration;
 import org.apache.shenyu.k8s.common.IngressConstants;
 import org.apache.shenyu.k8s.common.ShenyuMemoryConfig;
@@ -39,6 +43,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -145,6 +150,19 @@ public class IngressParserTest {
         assertEquals(Arrays.asList(PluginEnum.SOFA.getName()), 
pluginNames(configs));
     }
 
+    @Test
+    public void testDispatchToExternalDefinitionWhenMatched() {
+        ingressParser = new IngressParser(mockInformer(), mockInformer(), 
Collections.singletonList(new FakeIngressPluginDefinition()));
+        Map<String, String> annotations = new HashMap<>();
+        annotations.put("shenyu.apache.org/plugin-fake-enabled", "true");
+        annotations.put(IngressConstants.PLUGIN_DUBBO_ENABLED, "true");
+
+        List<ShenyuMemoryConfig> configs = parse(annotations, "Exact");
+
+        assertEquals(1, configs.size());
+        assertEquals(Collections.singletonList("fake"), pluginNames(configs));
+    }
+
     @Test
     public void testDispatchToDivideWhenAllPluginAnnotationsDisabled() {
         Map<String, String> annotations = new HashMap<>();
@@ -192,4 +210,43 @@ public class IngressParserTest {
                 .map(SelectorData::getPluginName)
                 .collect(Collectors.toList());
     }
+
+    @SuppressWarnings("unchecked")
+    private SharedIndexInformer mockInformer() {
+        SharedIndexInformer informer = mock(SharedIndexInformer.class);
+        when(informer.getIndexer()).thenReturn(mock(Indexer.class));
+        return informer;
+    }
+
+    private static final class FakeIngressPluginDefinition implements 
IngressPluginDefinition {
+
+        @Override
+        public boolean matchesIngress(final V1Ingress ingress) {
+            return 
"true".equals(ingress.getMetadata().getAnnotations().get("shenyu.apache.org/plugin-fake-enabled"));
+        }
+
+        @Override
+        public String pluginName() {
+            return "fake";
+        }
+
+        @Override
+        public String contextPath(final V1Ingress ingress) {
+            return "/fake";
+        }
+
+        @Override
+        public PluginData pluginData(final V1Ingress ingress, final Request 
request, final Lister<V1Endpoints> endpointsLister) {
+            return 
PluginData.builder().id("9000").name("fake").config("{}").role(PluginRoleEnum.SYS.getName()).enabled(true).sort(9000).build();
+        }
+
+        @Override
+        public ShenyuMemoryConfig parse(final V1Ingress ingress, final 
CoreV1Api coreV1Api,
+                                        final Lister<V1Service> serviceLister, 
final Lister<V1Endpoints> endpointsLister) {
+            ShenyuMemoryConfig config = new ShenyuMemoryConfig();
+            SelectorData selectorData = 
SelectorData.builder().pluginName("fake").name("fake").enabled(true).build();
+            config.setRouteConfigList(Collections.singletonList(new 
IngressConfiguration(selectorData, Collections.emptyList(), 
Collections.emptyList())));
+            return config;
+        }
+    }
 }
diff --git 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
index fe16e76d8b..7548b9d524 100644
--- 
a/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
+++ 
b/shenyu-spring-boot-starter/shenyu-spring-boot-starter-k8s/src/main/java/org/apache/shenyu/springboot/starter/k8s/IngressControllerConfiguration.java
@@ -39,6 +39,7 @@ import org.apache.commons.collections4.MapUtils;
 import org.apache.shenyu.common.config.NettyHttpProperties;
 import org.apache.shenyu.common.config.ssl.ShenyuSniAsyncMapping;
 import org.apache.shenyu.common.exception.ShenyuException;
+import org.apache.shenyu.k8s.parser.IngressPluginDefinition;
 import org.apache.shenyu.k8s.parser.IngressParser;
 import org.apache.shenyu.k8s.reconciler.EndpointsReconciler;
 import org.apache.shenyu.k8s.reconciler.IngressReconciler;
@@ -56,10 +57,11 @@ import reactor.netty.tcp.TcpSslContextSpec;
 import java.io.ByteArrayInputStream;
 import java.io.InputStream;
 import java.time.Duration;
-
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.stream.Collectors;
 import java.util.concurrent.Executors;
 
 /**
@@ -182,11 +184,14 @@ public class IngressControllerConfiguration {
      *
      * @param serviceInformer serviceInformer
      * @param endpointsInformer endpointsInformer
+     * @param pluginDefinitions external plugin definitions
      * @return IngressParser
      */
     @Bean
-    public IngressParser ingressParser(final SharedIndexInformer<V1Service> 
serviceInformer, final SharedIndexInformer<V1Endpoints> endpointsInformer) {
-        return new IngressParser(serviceInformer, endpointsInformer);
+    public IngressParser ingressParser(final SharedIndexInformer<V1Service> 
serviceInformer, final SharedIndexInformer<V1Endpoints> endpointsInformer,
+                                       final 
ObjectProvider<IngressPluginDefinition> pluginDefinitions) {
+        List<IngressPluginDefinition> orderedDefinitions = 
pluginDefinitions.orderedStream().collect(Collectors.toList());
+        return new IngressParser(serviceInformer, endpointsInformer, 
orderedDefinitions);
     }
 
     /**

Reply via email to