This is an automated email from the ASF dual-hosted git repository.
ronething pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
The following commit(s) were added to refs/heads/master by this push:
new 15932856 feat: support plugin config annotations for ingress (#2627)
15932856 is described below
commit 159328561d2e61091247a26a6df7eae31c02035a
Author: Ashing Zheng <[email protected]>
AuthorDate: Wed Oct 29 10:53:57 2025 +0800
feat: support plugin config annotations for ingress (#2627)
---
internal/adc/translator/annotations.go | 15 ++-
.../annotations/pluginconfig/pluginconfig.go | 27 ++++
internal/adc/translator/ingress.go | 47 ++++++-
internal/controller/indexer/indexer.go | 20 +++
internal/controller/ingress_controller.go | 149 +++++++++++++++++++++
internal/webhook/v1/ingress_webhook.go | 1 -
test/e2e/ingress/annotations.go | 67 +++++++++
7 files changed, 315 insertions(+), 11 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..5b6935f5 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,51 @@ func buildRouteFromIngressPath(
uris = []string{"/*"}
}
}
- if config != nil && len(config.Plugins) > 0 {
- route.Plugins = config.Plugins
+
+ 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..f6305791 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,15 @@ func setupIngressIndexer(mgr ctrl.Manager) error {
return err
}
+ if err := mgr.GetFieldIndexer().IndexField(
+ context.Background(),
+ &networkingv1.Ingress{},
+ PluginConfigIndexRef,
+ IngressPluginConfigIndexFunc,
+ ); err != nil {
+ return err
+ }
+
return nil
}
@@ -932,3 +942,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
+ }
+
+ return []string{GenIndexKey(ingress.GetNamespace(), pluginConfigName)}
+}
diff --git a/internal/controller/ingress_controller.go
b/internal/controller/ingress_controller.go
index 65b1a5c7..7d5bfbf7 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)
@@ -360,6 +371,36 @@ func (r *IngressReconciler) listIngressesBySecret(ctx
context.Context, obj clien
}
}
+ // check if the secret is used by ApisixPluginConfig
+ var pluginConfigList apiv2.ApisixPluginConfigList
+ if err := r.List(ctx, &pluginConfigList, client.MatchingFields{
+ indexer.SecretIndexRef: indexer.GenIndexKey(namespace, name),
+ }); err != nil {
+ r.Log.Error(err, "failed to list plugin configs by secret",
"secret", name)
+ } else {
+ // For each PluginConfig, find Ingresses that reference it
+ for _, pc := range pluginConfigList.Items {
+ 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.GetName())
+ continue
+ }
+
+ for _, ingress := range ingressList.Items {
+ if MatchesIngressClass(r.Client, r.Log,
&ingress) {
+ requests = append(requests,
reconcile.Request{
+ NamespacedName:
client.ObjectKey{
+ Namespace:
ingress.Namespace,
+ Name: ingress.Name,
+ },
+ })
+ }
+ }
+ }
+ }
+
return distinctRequests(requests)
}
@@ -562,6 +603,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 +752,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/internal/webhook/v1/ingress_webhook.go
b/internal/webhook/v1/ingress_webhook.go
index 733e99f1..6f7a6229 100644
--- a/internal/webhook/v1/ingress_webhook.go
+++ b/internal/webhook/v1/ingress_webhook.go
@@ -40,7 +40,6 @@ var ingresslog = logf.Log.WithName("ingress-resource")
// ref:
https://apisix.apache.org/docs/ingress-controller/upgrade-guide/#limited-support-for-ingress-annotations
var unsupportedAnnotations = []string{
"k8s.apisix.apache.org/use-regex",
- "k8s.apisix.apache.org/plugin-config-name",
"k8s.apisix.apache.org/rewrite-target",
"k8s.apisix.apache.org/rewrite-target-regex",
"k8s.apisix.apache.org/rewrite-target-regex-template",
diff --git a/test/e2e/ingress/annotations.go b/test/e2e/ingress/annotations.go
index 739db09f..0a4857f5 100644
--- a/test/e2e/ingress/annotations.go
+++ b/test/e2e/ingress/annotations.go
@@ -429,5 +429,72 @@ spec:
Expect(err).NotTo(HaveOccurred(), "unmarshalling csrf
plugin config")
Expect(csrfConfig["key"]).To(Equal("foo-key"),
"checking csrf key")
})
+
+ 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")
+ })
})
})