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") + }) }) })
