This is an automated email from the ASF dual-hosted git repository.

ronething pushed a commit to branch feat/plugin_config_ingress
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git

commit aa05e3e92f0385e5be7992f83a1c92efbae8260d
Author: Ashing Zheng <[email protected]>
AuthorDate: Tue Oct 28 16:44:52 2025 +0800

    feat: support plugin config annotations for ingress
    
    Signed-off-by: Ashing Zheng <[email protected]>
---
 internal/adc/translator/annotations.go             |  15 +--
 .../annotations/pluginconfig/pluginconfig.go       |  27 +++++
 internal/adc/translator/ingress.go                 |  50 ++++++++-
 internal/controller/indexer/indexer.go             |  21 ++++
 internal/controller/ingress_controller.go          | 119 +++++++++++++++++++++
 test/e2e/ingress/annotations.go                    |  67 ++++++++++++
 6 files changed, 289 insertions(+), 10 deletions(-)

diff --git a/internal/adc/translator/annotations.go 
b/internal/adc/translator/annotations.go
index 9f92d43f..9f2efd0f 100644
--- a/internal/adc/translator/annotations.go
+++ b/internal/adc/translator/annotations.go
@@ -23,6 +23,7 @@ import (
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/pluginconfig"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/plugins"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/upstream"
        
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations/websocket"
@@ -30,15 +31,17 @@ import (
 
 // Structure extracted by Ingress Resource
 type IngressConfig struct {
-       Upstream        upstream.Upstream
-       Plugins         adctypes.Plugins
-       EnableWebsocket bool
+       Upstream         upstream.Upstream
+       Plugins          adctypes.Plugins
+       EnableWebsocket  bool
+       PluginConfigName string
 }
 
 var ingressAnnotationParsers = map[string]annotations.IngressAnnotationsParser{
-       "upstream":        upstream.NewParser(),
-       "plugins":         plugins.NewParser(),
-       "EnableWebsocket": websocket.NewParser(),
+       "upstream":         upstream.NewParser(),
+       "plugins":          plugins.NewParser(),
+       "EnableWebsocket":  websocket.NewParser(),
+       "PluginConfigName": pluginconfig.NewParser(),
 }
 
 func (t *Translator) TranslateIngressAnnotations(anno map[string]string) 
*IngressConfig {
diff --git a/internal/adc/translator/annotations/pluginconfig/pluginconfig.go 
b/internal/adc/translator/annotations/pluginconfig/pluginconfig.go
new file mode 100644
index 00000000..32513efd
--- /dev/null
+++ b/internal/adc/translator/annotations/pluginconfig/pluginconfig.go
@@ -0,0 +1,27 @@
+// 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 pluginconfig
+
+import 
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
+
+type pluginconfig struct{}
+
+func NewParser() annotations.IngressAnnotationsParser {
+       return &pluginconfig{}
+}
+
+func (w *pluginconfig) Parse(e annotations.Extractor) (any, error) {
+       return e.GetStringAnnotation(annotations.AnnotationsPluginConfigName), 
nil
+}
diff --git a/internal/adc/translator/ingress.go 
b/internal/adc/translator/ingress.go
index 5332662d..0eb57efc 100644
--- a/internal/adc/translator/ingress.go
+++ b/internal/adc/translator/ingress.go
@@ -161,7 +161,7 @@ func (t *Translator) buildServiceFromIngressPath(
        protocol := t.resolveIngressUpstream(tctx, obj, config, 
path.Backend.Service, upstream)
        service.Upstream = upstream
 
-       route := buildRouteFromIngressPath(obj, path, config, index, labels)
+       route := t.buildRouteFromIngressPath(tctx, obj, path, config, index, 
labels)
        // Check if websocket is enabled via annotation first, then fall back 
to appProtocol detection
        if config != nil && config.EnableWebsocket {
                route.EnableWebsocket = ptr.To(true)
@@ -248,7 +248,8 @@ func (t *Translator) resolveIngressUpstream(
        return protocol
 }
 
-func buildRouteFromIngressPath(
+func (t *Translator) buildRouteFromIngressPath(
+       tctx *provider.TranslateContext,
        obj *networkingv1.Ingress,
        path *networkingv1.HTTPIngressPath,
        config *IngressConfig,
@@ -279,13 +280,54 @@ func buildRouteFromIngressPath(
                        uris = []string{"/*"}
                }
        }
-       if config != nil && len(config.Plugins) > 0 {
-               route.Plugins = config.Plugins
+
+       // Load plugins from config
+       if config != nil {
+               // check if PluginConfig is specified
+               if config.PluginConfigName != "" {
+                       route.Plugins = 
t.loadPluginConfigPluginsForIngress(tctx, obj.Namespace, 
config.PluginConfigName)
+               }
+
+               // apply plugins from annotations
+               if len(config.Plugins) > 0 {
+                       if route.Plugins == nil {
+                               route.Plugins = make(adctypes.Plugins)
+                       }
+                       for k, v := range config.Plugins {
+                               route.Plugins[k] = v
+                       }
+               }
        }
+
        route.Uris = uris
        return route
 }
 
+func (t *Translator) loadPluginConfigPluginsForIngress(tctx 
*provider.TranslateContext, namespace, pluginConfigName string) 
adctypes.Plugins {
+       plugins := make(adctypes.Plugins)
+
+       pcKey := types.NamespacedName{
+               Namespace: namespace,
+               Name:      pluginConfigName,
+       }
+
+       pc, ok := tctx.ApisixPluginConfigs[pcKey]
+       if !ok || pc == nil {
+               return plugins
+       }
+
+       for _, plugin := range pc.Spec.Plugins {
+               if !plugin.Enable {
+                       continue
+               }
+
+               config := t.buildPluginConfig(plugin, namespace, tctx.Secrets)
+               plugins[plugin.Name] = config
+       }
+
+       return plugins
+}
+
 // translateEndpointSliceForIngress create upstream nodes from EndpointSlice
 func (t *Translator) translateEndpointSliceForIngress(weight int, 
endpointSlices []discoveryv1.EndpointSlice, servicePort *corev1.ServicePort) 
adctypes.UpstreamNodes {
        nodes := adctypes.UpstreamNodes{}
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index f517e11d..70dcb920 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -32,6 +32,7 @@ import (
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
        internaltypes 
"github.com/apache/apisix-ingress-controller/internal/types"
 )
 
@@ -425,6 +426,16 @@ func setupIngressIndexer(mgr ctrl.Manager) error {
                return err
        }
 
+       // create PluginConfig index for quick lookup of Ingresses using 
specific plugin configs
+       if err := mgr.GetFieldIndexer().IndexField(
+               context.Background(),
+               &networkingv1.Ingress{},
+               PluginConfigIndexRef,
+               IngressPluginConfigIndexFunc,
+       ); err != nil {
+               return err
+       }
+
        return nil
 }
 
@@ -932,3 +943,13 @@ func ApisixGlobalRuleSecretIndexFunc(rawObj client.Object) 
[]string {
        }
        return keys
 }
+
+func IngressPluginConfigIndexFunc(rawObj client.Object) []string {
+       ingress := rawObj.(*networkingv1.Ingress)
+       pluginConfigName := 
ingress.Annotations[annotations.AnnotationsPluginConfigName]
+       if pluginConfigName == "" {
+               return nil
+       }
+       // PluginConfig is in the same namespace as the Ingress
+       return []string{GenIndexKey(ingress.GetNamespace(), pluginConfigName)}
+}
diff --git a/internal/controller/ingress_controller.go 
b/internal/controller/ingress_controller.go
index 65b1a5c7..70caf45e 100644
--- a/internal/controller/ingress_controller.go
+++ b/internal/controller/ingress_controller.go
@@ -41,6 +41,8 @@ import (
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        "github.com/apache/apisix-ingress-controller/api/v1alpha1"
+       apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+       
"github.com/apache/apisix-ingress-controller/internal/adc/translator/annotations"
        
"github.com/apache/apisix-ingress-controller/internal/controller/indexer"
        "github.com/apache/apisix-ingress-controller/internal/controller/status"
        "github.com/apache/apisix-ingress-controller/internal/manager/readiness"
@@ -107,6 +109,9 @@ func (r *IngressReconciler) SetupWithManager(mgr 
ctrl.Manager) error {
                Watches(&v1alpha1.GatewayProxy{},
                        
handler.EnqueueRequestsFromMapFunc(r.listIngressesForGatewayProxy),
                ).
+               Watches(&apiv2.ApisixPluginConfig{},
+                       
handler.EnqueueRequestsFromMapFunc(r.listIngressesForPluginConfig),
+               ).
                WatchesRawSource(
                        source.Channel(
                                r.genericEvent,
@@ -183,6 +188,12 @@ func (r *IngressReconciler) Reconcile(ctx context.Context, 
req ctrl.Request) (ct
                return ctrl.Result{}, err
        }
 
+       // process plugin config annotation
+       if err := r.processPluginConfig(tctx, ingress); err != nil {
+               r.Log.Error(err, "failed to process PluginConfig annotation", 
"ingress", ingress.Name)
+               return ctrl.Result{}, err
+       }
+
        // process HTTPRoutePolicy
        if err := r.processHTTPRoutePolicies(tctx, ingress); err != nil {
                r.Log.Error(err, "failed to process HTTPRoutePolicy", 
"ingress", ingress.Name)
@@ -562,6 +573,73 @@ func (r *IngressReconciler) processBackendService(tctx 
*provider.TranslateContex
        return nil
 }
 
+// processPluginConfig process the plugin config annotation of the ingress
+func (r *IngressReconciler) processPluginConfig(tctx 
*provider.TranslateContext, ingress *networkingv1.Ingress) error {
+       pluginConfigName := 
ingress.Annotations[annotations.AnnotationsPluginConfigName]
+       if pluginConfigName == "" {
+               return nil
+       }
+
+       var (
+               pc = apiv2.ApisixPluginConfig{
+                       ObjectMeta: metav1.ObjectMeta{
+                               Name:      pluginConfigName,
+                               Namespace: ingress.Namespace,
+                       },
+               }
+               pcNN = utils.NamespacedName(&pc)
+       )
+
+       if err := r.Get(tctx, pcNN, &pc); err != nil {
+               r.Log.Error(err, "failed to get ApisixPluginConfig", 
"pluginconfig", pcNN)
+               return err
+       }
+
+       // Check if ApisixPluginConfig has IngressClassName and if it matches
+       if pc.Spec.IngressClassName != "" {
+               ingressClassName := 
internaltypes.GetEffectiveIngressClassName(ingress)
+               if ingressClassName != pc.Spec.IngressClassName {
+                       var pcIC networkingv1.IngressClass
+                       if err := r.Get(tctx, client.ObjectKey{Name: 
pc.Spec.IngressClassName}, &pcIC); err != nil {
+                               r.Log.Error(err, "failed to get IngressClass 
for ApisixPluginConfig", "ingressclass", pc.Spec.IngressClassName, 
"pluginconfig", pcNN)
+                               return nil
+                       }
+                       if !matchesController(pcIC.Spec.Controller) {
+                               r.Log.V(1).Info("ApisixPluginConfig references 
IngressClass with non-matching controller", "pluginconfig", pcNN, 
"ingressclass", pc.Spec.IngressClassName)
+                               return nil
+                       }
+               }
+       }
+
+       tctx.ApisixPluginConfigs[pcNN] = &pc
+
+       // Also check secrets referenced by plugin config
+       for _, plugin := range pc.Spec.Plugins {
+               if !plugin.Enable {
+                       continue
+               }
+               if plugin.SecretRef == "" {
+                       continue
+               }
+               var (
+                       secret = corev1.Secret{
+                               ObjectMeta: metav1.ObjectMeta{
+                                       Name:      plugin.SecretRef,
+                                       Namespace: ingress.Namespace,
+                               },
+                       }
+                       secretNN = utils.NamespacedName(&secret)
+               )
+               if err := r.Get(tctx, secretNN, &secret); err != nil {
+                       r.Log.Error(err, "failed to get Secret for 
ApisixPluginConfig", "secret", secretNN, "pluginconfig", pcNN)
+                       continue
+               }
+               tctx.Secrets[secretNN] = &secret
+       }
+
+       return nil
+}
+
 // updateStatus update the status of the ingress
 func (r *IngressReconciler) updateStatus(ctx context.Context, tctx 
*provider.TranslateContext, ingress *networkingv1.Ingress, ingressClass 
*networkingv1.IngressClass) error {
        var loadBalancerStatus networkingv1.IngressLoadBalancerStatus
@@ -644,3 +722,44 @@ func (r *IngressReconciler) updateStatus(ctx 
context.Context, tctx *provider.Tra
 func (r *IngressReconciler) listIngressesForGatewayProxy(ctx context.Context, 
obj client.Object) []reconcile.Request {
        return listIngressClassRequestsForGatewayProxy(ctx, r.Client, obj, 
r.Log, r.listIngressForIngressClass)
 }
+
+// listIngressesForPluginConfig list all ingresses that use a specific plugin 
config
+func (r *IngressReconciler) listIngressesForPluginConfig(ctx context.Context, 
obj client.Object) []reconcile.Request {
+       pc, ok := obj.(*apiv2.ApisixPluginConfig)
+       if !ok {
+               r.Log.Error(fmt.Errorf("unexpected object type"), "failed to 
convert object to ApisixPluginConfig")
+               return nil
+       }
+
+       // First check if the ApisixPluginConfig has matching IngressClassName
+       if pc.Spec.IngressClassName != "" {
+               var ic networkingv1.IngressClass
+               if err := r.Get(ctx, client.ObjectKey{Name: 
pc.Spec.IngressClassName}, &ic); err != nil {
+                       if client.IgnoreNotFound(err) != nil {
+                               r.Log.Error(err, "failed to get IngressClass 
for ApisixPluginConfig", "pluginconfig", pc.Name)
+                       }
+                       return nil
+               }
+               if !matchesController(ic.Spec.Controller) {
+                       return nil
+               }
+       }
+
+       var ingressList networkingv1.IngressList
+       if err := r.List(ctx, &ingressList, client.MatchingFields{
+               indexer.PluginConfigIndexRef: 
indexer.GenIndexKey(pc.GetNamespace(), pc.GetName()),
+       }); err != nil {
+               r.Log.Error(err, "failed to list ingresses by plugin config", 
"pluginconfig", pc.Name)
+               return nil
+       }
+
+       requests := make([]reconcile.Request, 0, len(ingressList.Items))
+       for _, ingress := range ingressList.Items {
+               if MatchesIngressClass(r.Client, r.Log, &ingress) {
+                       requests = append(requests, reconcile.Request{
+                               NamespacedName: utils.NamespacedName(&ingress),
+                       })
+               }
+       }
+       return requests
+}
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 3128cb04..b0623603 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -359,5 +359,72 @@ spec:
                                Status(http.StatusPermanentRedirect).
                                Header("Location").IsEqual("/anything/ip")
                })
+
+               It("plugin-config-name annotation", func() {
+                       // Create ApisixPluginConfig
+                       pluginConfig := `
+apiVersion: apisix.apache.org/v2
+kind: ApisixPluginConfig
+metadata:
+  name: test-plugin-config
+spec:
+  ingressClassName: %s
+  plugins:
+  - name: echo
+    enable: true
+    config:
+      body: "hello from plugin config"
+`
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(pluginConfig, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating ApisixPluginConfig")
+
+                       // Create Ingress with plugin-config-name annotation
+                       ingressWithPluginConfig := `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: plugin-config-test
+  annotations:
+    k8s.apisix.apache.org/plugin-config-name: "test-plugin-config"
+spec:
+  ingressClassName: %s
+  rules:
+  - host: plugin-config.example
+    http:
+      paths:
+      - path: /get
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+                       
Expect(s.CreateResourceFromString(fmt.Sprintf(ingressWithPluginConfig, 
s.Namespace()))).ShouldNot(HaveOccurred(), "creating Ingress")
+
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Host:   "plugin-config.example",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedBodyContains("hello from plugin config"),
+                               },
+                       })
+
+                       routes, err := 
s.DefaultDataplaneResource().Route().List(context.Background())
+                       Expect(err).NotTo(HaveOccurred(), "listing Route")
+                       Expect(routes).ToNot(BeEmpty(), "checking Route length")
+
+                       Expect(routes).To(HaveLen(1), "checking Route length")
+                       Expect(routes[0].Plugins).To(HaveKey("echo"), "checking 
Route has echo plugin from PluginConfig")
+
+                       // Verify plugin config content
+                       jsonBytes, err := 
json.Marshal(routes[0].Plugins["echo"])
+                       Expect(err).NotTo(HaveOccurred(), "marshalling echo 
plugin config")
+                       var echoConfig map[string]any
+                       err = json.Unmarshal(jsonBytes, &echoConfig)
+                       Expect(err).NotTo(HaveOccurred(), "unmarshalling echo 
plugin config")
+                       Expect(echoConfig["body"]).To(Equal("hello from plugin 
config"), "checking echo plugin body")
+               })
        })
 })

Reply via email to